diff --git a/CHANGELOG.md b/CHANGELOG.md index df27c49..feb9413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +## v1.5.0.0 + +- 增加韩语密文 +- 微调UI、提示 + ## v1.4.0.0 - 增加日语密文 diff --git a/README.md b/README.md index eb020df..4feaf4a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # 想曰 [![GitHub stars](https://img.shields.io/github/stars/fzxx/XiangYue?style=social)](https://github.com/fzxx/XiangYue) [![GitHub forks](https://img.shields.io/github/forks/fzxx/XiangYue?style=social&logo=github)](https://github.com/fzxx/XiangYue) [![已关闭 Issues](https://img.shields.io/github/issues-closed/fzxx/XiangYue?label=已解决问题&color=green&logo=github)](https://github.com/fzxx/XiangYue/issues?q=is%3Aissue+is%3Aclosed) [![许可证](https://img.shields.io/badge/License-私下研究专用许可-blue?logo=github)](https://github.com/fzxx/XiangYue/blob/main/main/License.txt) -[![级联算法](https://img.shields.io/badge/%E6%96%87%E6%9C%AC%E5%8A%A0%E5%AF%86%E9%A6%96%E4%B8%AA%E7%BA%A7%E8%81%94%E7%AE%97%E6%B3%95%E5%B7%A5%E5%85%B7-AES256%20CTR%20+%20ChaCha20%20Poly1305%20IETF-purple?logo=cryptpad&logoColor=white)](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://img.shields.io/badge/%E6%94%AF%E6%8C%81%E5%A4%9A%E7%A7%8D%E5%AF%86%E6%96%87-%E4%B8%AD%E6%96%87/Base64/Emoji/%E9%9B%B6%E5%AE%BD/%E6%97%A5%E8%AF%AD-purple?logo=livechat&logoColor=white)](https://github.com/fzxx/XiangYue?tab=readme-ov-file#-%E7%89%B9%E7%82%B9) +[![级联算法](https://img.shields.io/badge/%E6%96%87%E6%9C%AC%E5%8A%A0%E5%AF%86%E9%A6%96%E4%B8%AA%E7%BA%A7%E8%81%94%E7%AE%97%E6%B3%95%E5%B7%A5%E5%85%B7-AES256%20CTR%20+%20ChaCha20%20Poly1305%20IETF-purple?logo=cryptpad&logoColor=white)](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://img.shields.io/badge/%E6%94%AF%E6%8C%81%E5%A4%9A%E7%A7%8D%E5%AF%86%E6%96%87-%E4%B8%AD%E6%96%87/Base64/Emoji/%E9%9B%B6%E5%AE%BD/%E6%97%A5%E8%AF%AD/%E9%9F%A9%E8%AF%AD-purple?logo=livechat&logoColor=white)](https://github.com/fzxx/XiangYue?tab=readme-ov-file#-%E7%89%B9%E7%82%B9) 想曰(yuē) 是基于现代加密技术的文本加密工具,使用**多算法级联加密**方案,确保数据在本地完成加密/解密,保护隐私安全。 ## 🌟 特点 -- ㊙️**密文**:支持 `中文/Base64/Emoji/零宽/日语` 密文 +- ㊙️**密文**:支持 `中文/Base64/Emoji/零宽/日语/韩语` 密文 - 🔐**密钥**:`Argon2id + HKDF-SHA512`,有效抵御暴力破解 - 🔒**级联算法**:采用 `AES256-CTR` 与 `ChaCha20-Poly1305-IETF` 级联加密,**安全性极高** - 📄**数据**:所有操作在本地完成,数据不离开设备 @@ -45,6 +45,12 @@ J7ni11NnCUEe1+GtZcIWoJcKNgzsyN8K8BQBKnDn/1mLPkv2ul1VUcedyoIgZpXcNUKfy3HhZI6soaa5 ヷㇴょわㇿデぞズゆェピゆベナこマびむしヾノざュゝるスしニユダクぷすゾゔうゼダりち〴ㇹぐぁぇヘぷゼぺにづヂボゔㇲこぱミみぼメェだは ``` +##### 韩语密文 + +```plaintext +퍼헬팁청쌀빠꽂뭇활현라골띤틀문헬룸로쿠텁완권ㅂ기멎끔해되릎펫닫궉뉴담답폼칼받듀릭맑일친끼죄루디ㄲ집멎블권대안지당톤사혐군즈꼭율한 +``` + ## 🖥️在线与离线使用 [![在线页面](https://img.shields.io/badge/在线页面-想曰-yellow?logo=googlechrome&logoColor=white)](https://xyue.515188.xyz/) [![免尴尬页面](https://img.shields.io/badge/免尴尬页面-想说-green?logo=firefox&logoColor=white)](https://xshuo.515188.xyz/) [![Release](https://img.shields.io/github/v/release/fzxx/XiangYue?label=离线客户端&color=blue&logo=github)](https://github.com/fzxx/XiangYue/releases) @@ -83,7 +89,7 @@ J7ni11NnCUEe1+GtZcIWoJcKNgzsyN8K8BQBKnDn/1mLPkv2ul1VUcedyoIgZpXcNUKfy3HhZI6soaa5 #### 经过某些软件发送密文后,解密错误? -- 是因为**某些软件喜欢折叠聊天内容**,或者发送过长的密文被截断,建议你检查密文的完整性;Eomij密文可能因为不同设备内置的表情不一导致解密错误,零宽密文有被某些平台过滤字符的可能。 +- 是因为**某些软件喜欢折叠聊天内容**,或者发送过长的密文被截断,**安卓系统粘贴板有长度限制**,建议你检查密文的完整性;Eomij密文可能因为不同设备内置的表情不一导致解密错误,零宽密文有被某些平台过滤字符的可能。 #### 零宽密文只有两个字符?可见字符可以自定义? diff --git a/SECURITY.md b/SECURITY.md index d182ed7..e6aeffb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -66,6 +66,9 @@ IV(16字节)、Nonce(12字节),随机 | AES-CTR IV | 16 字节 | 随机种子派生 | 计数器初始值 | | ChaCha20 Nonce | 12 字节 | 随机种子派生 | 一次性随机数 | +- 设备需保证安全,无病毒木马、无可疑的窃听软件 +- 包括不限于**不受信任的浏览器、输入法、杀毒软件** + ### 漏洞报告 [![漏洞报告](https://img.shields.io/badge/%E6%BC%8F%E6%B4%9E%E6%8A%A5%E5%91%8A-gold?style=for-the-badge&logo=github&&logoColor=black)](https://github.com/fzxx/XiangYue/issues) diff --git a/favicon.ico b/favicon.ico index 13eb436..273ae29 100644 Binary files a/favicon.ico and b/favicon.ico differ diff --git a/index.html b/index.html index d8c19eb..764a040 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -想曰 - Want To Say

密码建议:包含大小写字母、数字和特殊符号,长度至少16位;或者点击随机生成按钮。

使用提示
点击导航栏的图标可以切换非对称加密、设置输出的密文、切换夜间模式

密钥算法:Argon2id ➕ HKDF-SHA512 加密算法:AES256-CTR ➕ ChaCha20-Poly1305-IETF

所有操作均在本设备内完成;请妥善保存密码,丢失将无法解密数据。

非对称加密提示
使用公钥加密,私钥解密;私钥签名,公钥验证

算法:ECDH

请用密码管理器妥善保存你的私钥,丢失将无法解密数据;不要向任何人透露私钥。

\ No newline at end of file +想曰 - Want To Say

密码建议:包含大小写字母、数字和特殊符号,长度至少16位;或者点击随机生成按钮。

使用提示
点击导航栏的图标可以切换非对称加密、设置输出的密文、切换夜间模式

密钥算法:Argon2id ➕ HKDF-SHA512 加密算法:AES256-CTR ➕ ChaCha20-Poly1305-IETF

所有操作均在本设备内完成;请妥善保存密码,丢失将无法解密数据。

非对称加密提示
使用公钥加密,私钥解密;私钥签名,公钥验证

算法:ECDH

请用密码管理器妥善保存你的私钥,丢失将无法解密数据;不要向任何人透露私钥。

\ No newline at end of file diff --git a/js/encryption-method-1.js b/js/encryption-method-1.js index bebb562..cae841c 100644 --- a/js/encryption-method-1.js +++ b/js/encryption-method-1.js @@ -1,2 +1,2 @@ -const encryptionMethod1={async deriveMasterKey(password,seed){try{const pwdBuf=utils.stringToArrayBuffer(password);const masterBits=sodium.crypto_pwhash(64,new Uint8Array(pwdBuf),new Uint8Array(seed),sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,sodium.crypto_pwhash_ALG_ARGON2ID13);const hkdfKey=await crypto.subtle.importKey('raw',masterBits,{name:'HKDF'},false,['deriveBits']);const[aesKeyBits,chachaKeyBits,aesIv,chachaNonce]=await Promise.all([crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-512',salt:seed,info:utils.stringToArrayBuffer('AES-CTR-Key')},hkdfKey,256),crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-512',salt:seed,info:utils.stringToArrayBuffer('ChaCha20-Key')},hkdfKey,256),crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-512',salt:seed,info:utils.stringToArrayBuffer('AES-CTR-IV')},hkdfKey,128),crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-512',salt:seed,info:utils.stringToArrayBuffer('ChaCha20-Nonce')},hkdfKey,96)]);const aesCtrKey=await crypto.subtle.importKey('raw',aesKeyBits,{name:'AES-CTR'},false,['encrypt','decrypt']);return{aesCtrKey,chacha20Key:new Uint8Array(chachaKeyBits),aesCtrIv:new Uint8Array(aesIv),chacha20Nonce:new Uint8Array(chachaNonce)};}catch(e){throw new Error(`主密钥派生失败: ${e.message}`);}},async deriveKeyPBKDF2(password,salt){try{const pwdBuf=utils.stringToArrayBuffer(password);const pbkdf2Key=await crypto.subtle.importKey('raw',pwdBuf,{name:'PBKDF2'},false,['deriveBits']);const keyBits=await crypto.subtle.deriveBits({name:'PBKDF2',salt,iterations:500000,hash:'SHA-256'},pbkdf2Key,512);const hkdfKey=await crypto.subtle.importKey('raw',keyBits,{name:'HKDF'},false,['deriveBits']);const[aesKeyBits,chachaKeyBits]=await Promise.all([crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-256',salt:new Uint8Array(0),info:utils.stringToArrayBuffer('AES-CTR')},hkdfKey,256),crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-256',salt:new Uint8Array(0),info:utils.stringToArrayBuffer('ChaCha20')},hkdfKey,256)]);const aesCtrKey=await crypto.subtle.importKey('raw',aesKeyBits,{name:'AES-CTR'},false,['encrypt','decrypt']);return{aesCtrKey,chacha20Key:new Uint8Array(chachaKeyBits)};}catch(e){throw new Error(`PBKDF2密钥派生失败: ${e.message}`);}},async encryptAESCTR(plaintext,key,iv){try{return crypto.subtle.encrypt({name:'AES-CTR',counter:new Uint8Array(iv),length:128},key,plaintext);}catch(e){throw new Error(`AES-CTR加密失败: ${e.message}`);}},async decryptAESCTR(ciphertext,key,iv){try{return crypto.subtle.decrypt({name:'AES-CTR',counter:new Uint8Array(iv),length:128},key,ciphertext);}catch(e){throw new Error(`AES-CTR解密失败: ${e.message}`);}},async encryptChaCha20Poly1305(plaintext,key,nonce,assocData){try{return sodium.crypto_aead_chacha20poly1305_ietf_encrypt(new Uint8Array(plaintext),new Uint8Array(assocData),null,new Uint8Array(nonce),key);}catch(e){throw new Error(`ChaCha20-Poly1305加密失败: ${e.message}`);}},async decryptChaCha20Poly1305(ciphertext,key,nonce,assocData){try{const decData=sodium.crypto_aead_chacha20poly1305_ietf_decrypt(null,new Uint8Array(ciphertext),new Uint8Array(assocData),new Uint8Array(nonce),key);return decData.buffer;}catch(e){throw new Error(`ChaCha20-Poly1305解密失败: ${e.message}`);}},async encrypt(plaintext,password,outputMode='base64'){try{const seed=utils.generateRandomBytes(16);const[masterKey,compData]=await Promise.all([this.deriveMasterKey(password,seed),utils.compressData(utils.stringToArrayBuffer(plaintext))]);const aesEnc=await this.encryptAESCTR(compData,masterKey.aesCtrKey,masterKey.aesCtrIv);const chachaEnc=await this.encryptChaCha20Poly1305(aesEnc,masterKey.chacha20Key,masterKey.chacha20Nonce,seed);const outBuf=new Uint8Array(seed.byteLength+chachaEnc.byteLength);outBuf.set(new Uint8Array(seed),0);outBuf.set(new Uint8Array(chachaEnc),seed.byteLength);let result=utils.arrayBufferToBase64(outBuf.buffer);const fmtMap={chinese:mappingMode1.Base64ToChinese,emoji:mappingMode2.Base64ToEmoji,'zero-width':mappingMode3.Base64ToZeroWidth,japanese:mappingMode4.Base64ToJapanese};return fmtMap[outputMode]?fmtMap[outputMode](result):result;}catch(e){throw new Error(`加密过程发生错误: ${e.message}`);}},async decrypt(ciphertext,password){try{const ctxType=utils.detectCiphertextType(ciphertext);const fmtMap={emoji:mappingMode2.emojiToBase64,chinese:mappingMode1.ChineseToBase64,'zero-width':mappingMode3.zeroWidthToBase64,japanese:mappingMode4.JapaneseToBase64};let procCtx=ciphertext;if(fmtMap[ctxType])procCtx=fmtMap[ctxType](procCtx);else if(['base64','weibase64'].includes(ctxType))procCtx=procCtx.replace(/[^A-Za-z0-9+/=]/g,'');const ctxBuf=utils.Base64ToArrayBuffer(procCtx);let plaintext;try{const seed=ctxBuf.slice(0,16);const masterKey=await this.deriveMasterKey(password,seed);const chachaDec=await this.decryptChaCha20Poly1305(ctxBuf.slice(16),masterKey.chacha20Key,masterKey.chacha20Nonce,seed);plaintext=await this.decryptAESCTR(chachaDec,masterKey.aesCtrKey,masterKey.aesCtrIv);}catch(e){const salt=ctxBuf.slice(0,16);const nonce=ctxBuf.slice(16,28);const[aesKey,chachaKey]=await Promise.all([this.deriveKeyPBKDF2(password,salt).then(k=>k.aesCtrKey),this.deriveKeyPBKDF2(password,salt).then(k=>k.chacha20Key)]);const chachaDec=await this.decryptChaCha20Poly1305(ctxBuf.slice(28),chachaKey,nonce);plaintext=await this.decryptAESCTR(chachaDec.slice(16),aesKey,chachaDec.slice(0,16));} +const encryptionMethod1={async deriveMasterKey(password,seed){try{const pwdBuf=utils.stringToArrayBuffer(password);const masterBits=sodium.crypto_pwhash(64,new Uint8Array(pwdBuf),new Uint8Array(seed),sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,sodium.crypto_pwhash_ALG_ARGON2ID13);const hkdfKey=await crypto.subtle.importKey('raw',masterBits,{name:'HKDF'},false,['deriveBits']);const[aesKeyBits,chachaKeyBits,aesIv,chachaNonce]=await Promise.all([crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-512',salt:seed,info:utils.stringToArrayBuffer('AES-CTR-Key')},hkdfKey,256),crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-512',salt:seed,info:utils.stringToArrayBuffer('ChaCha20-Key')},hkdfKey,256),crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-512',salt:seed,info:utils.stringToArrayBuffer('AES-CTR-IV')},hkdfKey,128),crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-512',salt:seed,info:utils.stringToArrayBuffer('ChaCha20-Nonce')},hkdfKey,96)]);const aesCtrKey=await crypto.subtle.importKey('raw',aesKeyBits,{name:'AES-CTR'},false,['encrypt','decrypt']);return{aesCtrKey,chacha20Key:new Uint8Array(chachaKeyBits),aesCtrIv:new Uint8Array(aesIv),chacha20Nonce:new Uint8Array(chachaNonce)};}catch(e){throw new Error(`主密钥派生失败: ${e.message}`);}},async deriveKeyPBKDF2(password,salt){try{const pwdBuf=utils.stringToArrayBuffer(password);const pbkdf2Key=await crypto.subtle.importKey('raw',pwdBuf,{name:'PBKDF2'},false,['deriveBits']);const keyBits=await crypto.subtle.deriveBits({name:'PBKDF2',salt,iterations:500000,hash:'SHA-256'},pbkdf2Key,512);const hkdfKey=await crypto.subtle.importKey('raw',keyBits,{name:'HKDF'},false,['deriveBits']);const[aesKeyBits,chachaKeyBits]=await Promise.all([crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-256',salt:new Uint8Array(0),info:utils.stringToArrayBuffer('AES-CTR')},hkdfKey,256),crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-256',salt:new Uint8Array(0),info:utils.stringToArrayBuffer('ChaCha20')},hkdfKey,256)]);const aesCtrKey=await crypto.subtle.importKey('raw',aesKeyBits,{name:'AES-CTR'},false,['encrypt','decrypt']);return{aesCtrKey,chacha20Key:new Uint8Array(chachaKeyBits)};}catch(e){throw new Error(`PBKDF2密钥派生失败: ${e.message}`);}},async encryptAESCTR(plaintext,key,iv){try{return crypto.subtle.encrypt({name:'AES-CTR',counter:new Uint8Array(iv),length:128},key,plaintext);}catch(e){throw new Error(`AES-CTR加密失败: ${e.message}`);}},async decryptAESCTR(ciphertext,key,iv){try{return crypto.subtle.decrypt({name:'AES-CTR',counter:new Uint8Array(iv),length:128},key,ciphertext);}catch(e){throw new Error(`AES-CTR解密失败: ${e.message}`);}},async encryptChaCha20Poly1305(plaintext,key,nonce,assocData){try{return sodium.crypto_aead_chacha20poly1305_ietf_encrypt(new Uint8Array(plaintext),new Uint8Array(assocData),null,new Uint8Array(nonce),key);}catch(e){throw new Error(`ChaCha20-Poly1305加密失败: ${e.message}`);}},async decryptChaCha20Poly1305(ciphertext,key,nonce,assocData){try{const decData=sodium.crypto_aead_chacha20poly1305_ietf_decrypt(null,new Uint8Array(ciphertext),new Uint8Array(assocData),new Uint8Array(nonce),key);return decData.buffer;}catch(e){throw new Error(`ChaCha20-Poly1305解密失败: ${e.message}`);}},async encrypt(plaintext,password,outputMode='base64'){try{const seed=sodium.randombytes_buf(16);const[masterKey,compData]=await Promise.all([this.deriveMasterKey(password,seed),utils.compressData(utils.stringToArrayBuffer(plaintext))]);const aesEnc=await this.encryptAESCTR(compData,masterKey.aesCtrKey,masterKey.aesCtrIv);const chachaEnc=await this.encryptChaCha20Poly1305(aesEnc,masterKey.chacha20Key,masterKey.chacha20Nonce,seed);const outBuf=new Uint8Array(seed.byteLength+chachaEnc.byteLength);outBuf.set(new Uint8Array(seed),0);outBuf.set(new Uint8Array(chachaEnc),seed.byteLength);let result=utils.arrayBufferToBase64(outBuf.buffer);const fmtMap={chinese:mappingMode1.Base64ToChinese,emoji:mappingMode2.Base64ToEmoji,'zero-width':mappingMode3.Base64ToZeroWidth,japanese:mappingMode4.Base64ToJapanese,korean:mappingMode5.Base64ToKorean};return fmtMap[outputMode]?fmtMap[outputMode](result):result;}catch(e){throw new Error(`加密过程发生错误: ${e.message}`);}},async decrypt(ciphertext,password){try{const ctxType=utils.detectCiphertextType(ciphertext);const fmtMap={emoji:mappingMode2.emojiToBase64,chinese:mappingMode1.ChineseToBase64,'zero-width':mappingMode3.zeroWidthToBase64,japanese:mappingMode4.JapaneseToBase64,korean:mappingMode5.KoreanToBase64};let procCtx=ciphertext;if(fmtMap[ctxType])procCtx=fmtMap[ctxType](procCtx);else if(['base64','weibase64'].includes(ctxType))procCtx=procCtx.replace(/[^A-Za-z0-9+/=]/g,'');const ctxBuf=utils.Base64ToArrayBuffer(procCtx);let plaintext;try{const seed=ctxBuf.slice(0,16);const masterKey=await this.deriveMasterKey(password,seed);const chachaDec=await this.decryptChaCha20Poly1305(ctxBuf.slice(16),masterKey.chacha20Key,masterKey.chacha20Nonce,seed);plaintext=await this.decryptAESCTR(chachaDec,masterKey.aesCtrKey,masterKey.aesCtrIv);}catch(e){const salt=ctxBuf.slice(0,16);const nonce=ctxBuf.slice(16,28);const[aesKey,chachaKey]=await Promise.all([this.deriveKeyPBKDF2(password,salt).then(k=>k.aesCtrKey),this.deriveKeyPBKDF2(password,salt).then(k=>k.chacha20Key)]);const chachaDec=await this.decryptChaCha20Poly1305(ctxBuf.slice(28),chachaKey,nonce);plaintext=await this.decryptAESCTR(chachaDec.slice(16),aesKey,chachaDec.slice(0,16));} return utils.arrayBufferToString(await utils.decompressData(plaintext));}catch(e){throw new Error(`解密过程发生错误: ${e.message}`);}}}; \ No newline at end of file diff --git a/js/mapping-mode-5.js b/js/mapping-mode-5.js new file mode 100644 index 0000000..c868cde --- /dev/null +++ b/js/mapping-mode-5.js @@ -0,0 +1,4 @@ +const CharSets5={"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 KR2B64={};for(const[b64,arr]of Object.entries(CharSets5)){const code=b64.charCodeAt(0);for(const ch of arr){KR2B64[ch]=code;}} +const mappingMode5={Base64ToKorean(b64){const out=[];for(const ch of b64){const pool=CharSets5[ch];if(pool)out.push(pool[(Math.random()*10)|0]);} +return out.join('');},KoreanToBase64(str){const out=[];for(const ch of str){const code=KR2B64[ch];if(code!==undefined)out.push(String.fromCharCode(code));} +return out.join('');}}; \ No newline at end of file diff --git a/js/utils.js b/js/utils.js index b7f15cc..88e9f42 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,7 +1,8 @@ -const DEFAULT_PASSWORD='a184f7b849ffffed24d266a30298c72ef2f5ad040db73bf37151fac767630728';const STORAGE_KEYS={THEME_MODE:'theme_mode',OUTPUT_MODE:'output_mode',SYMMETRIC_PASSWORD:'symmetric_password'};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 JAPANESE_REGEX=/[\u3040-\u309F\u30A0-\u30FF\uFF66-\uFF9F]/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');}})();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库未加载');} +const DEFAULT_PASSWORD='a184f7b849ffffed24d266a30298c72ef2f5ad040db73bf37151fac767630728';const STORAGE_KEYS={THEME_MODE:'theme_mode',OUTPUT_MODE:'output_mode',SYMMETRIC_PASSWORD:'symmetric_password'};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 JAPANESE_REGEX=/[\u3040-\u309F\u30A0-\u30FF\uFF66-\uFF9F]/u;const KOREAN_REGEX=/[\uAC00-\uD7AF]/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');}})();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)),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';} -if(JAPANESE_REGEX.test(limitedText)){return'japanese';}},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','japanese'];if(validModes.includes(mode)){localStorage.setItem(STORAGE_KEYS.OUTPUT_MODE,mode);}},getSavedOutputMode(){return localStorage.getItem(STORAGE_KEYS.OUTPUT_MODE)||'chinese';},savePassword(password){if(password){localStorage.setItem(STORAGE_KEYS.SYMMETRIC_PASSWORD,password);}else{localStorage.removeItem(STORAGE_KEYS.SYMMETRIC_PASSWORD);}},getSavedPassword(){return localStorage.getItem(STORAGE_KEYS.SYMMETRIC_PASSWORD);}}; \ No newline at end of file +if(JAPANESE_REGEX.test(limitedText)){return'japanese';} +if(KOREAN_REGEX.test(limitedText)){return'korean';}},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','japanese','korean'];if(validModes.includes(mode)){localStorage.setItem(STORAGE_KEYS.OUTPUT_MODE,mode);}},getSavedOutputMode(){return localStorage.getItem(STORAGE_KEYS.OUTPUT_MODE)||'chinese';},savePassword(password){if(password){localStorage.setItem(STORAGE_KEYS.SYMMETRIC_PASSWORD,password);}else{localStorage.removeItem(STORAGE_KEYS.SYMMETRIC_PASSWORD);}},getSavedPassword(){return localStorage.getItem(STORAGE_KEYS.SYMMETRIC_PASSWORD);}}; \ No newline at end of file diff --git a/js/web.js b/js/web.js index a616806..1036f91 100644 --- a/js/web.js +++ b/js/web.js @@ -13,5 +13,5 @@ function showNotification(message,isSuccess=true){const container=document.creat `;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','save-password','result-status','theme-toggle','base64-toggle','paste-btn','encryption-mode-toggle','card-container'].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=``;els.encryptionKey.value=utils.getSavedPassword()||'';const modes=['chinese','base64','emoji','zero-width','japanese'];const modeNames={chinese:'中文',base64:'Base64',emoji:'Emoji','zero-width':'零宽',japanese:'日语'};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',japanese:'fa-regular fa-sun'};let outputMode=utils.getSavedOutputMode();els.base64Toggle.innerHTML=``;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=``;},savePassword:()=>{const password=els.encryptionKey.value.trim();utils.savePassword(password);showNotification(password?'密码已存本地(需要确保设备安全),清空密码再保存即删除。':'本地密码已删除');},base64Toggle:()=>{outputMode=modes[(modes.indexOf(outputMode)+1)%modes.length];utils.saveOutputMode(outputMode);els.base64Toggle.innerHTML=``;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.resultStatus.classList.add('hidden');showNotification('已清空内容');},themeToggle:()=>{const isDark=document.documentElement.classList.toggle('dark');utils.saveThemeMode(isDark?'dark':'light');els.themeToggle.innerHTML=``;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));let isFirstClick=true;els.encryptionModeToggle?.addEventListener('click',function(){const cardContainer=els.cardContainer;if(!cardContainer)return;const icon=this.querySelector('i');if(isFirstClick){navigator.clipboard.writeText('https://github.com/fzxx/XiangYue').catch(()=>{});alert('非对称加密视 Github Stars(关注)量再考虑,太少人用没有增加的必要\n\nGithub地址已复制 https://github.com/fzxx/XiangYue\n\n再次点按钮预可览非对称加密页面。');isFirstClick=false;}else{cardContainer.classList.toggle('flipped');if(cardContainer.classList.contains('flipped')){icon.classList.remove('fa-exchange');icon.classList.add('fa-sync-alt');}else{icon.classList.remove('fa-sync-alt');icon.classList.add('fa-exchange');}}});}); \ No newline at end of file +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','save-password','result-status','theme-toggle','base64-toggle','paste-btn','encryption-mode-toggle','card-container'].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=``;els.encryptionKey.value=utils.getSavedPassword()||'';const modes=['chinese','base64','emoji','zero-width','japanese','korean'];const modeNames={chinese:'中文',base64:'Base64',emoji:'Emoji','zero-width':'零宽',japanese:'日语',korean:'韩语'};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',japanese:'fa-regular fa-sun',korean:'fa-solid fa-flag-checkered'};let outputMode=utils.getSavedOutputMode();els.base64Toggle.innerHTML=``;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=``;},savePassword:()=>{const password=els.encryptionKey.value.trim();utils.savePassword(password);showNotification(password?'密码已存本地(需要确保设备安全),清空密码再保存即删除。':'本地密码已删除');},base64Toggle:()=>{outputMode=modes[(modes.indexOf(outputMode)+1)%modes.length];utils.saveOutputMode(outputMode);els.base64Toggle.innerHTML=``;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.resultStatus.classList.add('hidden');showNotification('已清空内容');},themeToggle:()=>{const isDark=document.documentElement.classList.toggle('dark');utils.saveThemeMode(isDark?'dark':'light');els.themeToggle.innerHTML=``;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));let isFirstClick=true;els.encryptionModeToggle?.addEventListener('click',function(){const cardContainer=els.cardContainer;if(!cardContainer)return;const icon=this.querySelector('i');if(isFirstClick){navigator.clipboard.writeText('https://github.com/fzxx/XiangYue').catch(()=>{});alert('非对称加密视 Github Stars(关注)量再考虑,太少人用没有增加的必要\n\nGithub地址已复制 https://github.com/fzxx/XiangYue\n\n再次点按钮可预览非对称加密页面,如有建议请到Github反馈。');isFirstClick=false;}else{cardContainer.classList.toggle('flipped');if(cardContainer.classList.contains('flipped')){icon.classList.remove('fa-exchange');icon.classList.add('fa-sync-alt');}else{icon.classList.remove('fa-sync-alt');icon.classList.add('fa-exchange');}}});}); \ No newline at end of file