diff --git a/README.md b/README.md index 40ee566..1084ce3 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ ```plaintext J7ni11NnCUEe1+GtZcIWoJcKNgzsyN8K8BQBKnDn/1mLPkv2ul1VUcedyoIgZpXcNUKfy3HhZI6soaa54UcqLtJs52caSPuVo3EBOYvMqYS2 ``` +```plaintext +🧕🛕🐱🌉🛐🤴🌄🏸🚆🎇🤴🦈🛸🧭🚡💒🤑🚤🔁🚬💰🍣⛴️🎽🔣😚❣️♻️🍖🧺🚨⛪️🛁📞🍤👦🍊🦘🦀🚅💓🏏🚪☪️😠💲🦊🧭🐠🎻🪣🚢⏲️⏯️😒🗻🧂🚠👻💗🪲🦽🐍🚲⏭️⏸️😍🛖🫐🛫🥓👴🐪👰⏰🏬🍱🤎🧄ℹ️⚾️🉑🚐🕎🐪😜🦖🚭🦐👽🧎🍢🥦🧘🐄🥖🔢🏃🎸🍤♎️🌆🐆🌋🤍☮️🫓🐑 +``` + +## 在线与离线使用 + +[想曰](https://xyue.515188.xyz/) [想说(给怕尴尬的朋友)](https://xshuo.515188.xyz/) [离线客户端(安卓暂时还需要在线使用)](https://github.com/fzxx/XiangYue/releases) ## 🛡️ 技术细节 @@ -85,6 +92,10 @@ J7ni11NnCUEe1+GtZcIWoJcKNgzsyN8K8BQBKnDn/1mLPkv2ul1VUcedyoIgZpXcNUKfy3HhZI6soaa5 - 不影响速度的情况下,**建议5M以下**(测试支持50M+);超大的文本请使用压缩包或者其它方式加密。 +#### 可以给密文再次加密? + +- `明文 → emoji密文 → 中文密文 → ......` 可以这样无限套娃,但不会增加安全性,安全性取决于你的密码,因此不建议这样做。 + ## 📖 许可证 [想曰](https://github.com/fzxx/XiangYue) - [私下研究专用许可](https://github.com/fzxx/XiangYue?tab=License-1-ov-file#) diff --git a/index.html b/index.html index 15f3cda..e2a4717 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -想曰 - Want To Say

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

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

密钥算法:PBKDF2-SHA256 ➕ HKDF-SHA256 加密算法:AES-CTR ➕ ChaCha20-Poly1305-IETF

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

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

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

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

密钥算法:PBKDF2-SHA256 ➕ HKDF-SHA256 加密算法:AES-CTR ➕ ChaCha20-Poly1305-IETF

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

\ No newline at end of file diff --git a/js/encryption-method-1.js b/js/encryption-method-1.js index 614c9a5..e2dfb91 100644 --- a/js/encryption-method-1.js +++ b/js/encryption-method-1.js @@ -1,3 +1 @@ -const encryptionMethod1={async deriveKey(password,salt){try{const passwordBuffer=utils.stringToArrayBuffer(password);const pbkdf2Key=await crypto.subtle.importKey('raw',passwordBuffer,{name:'PBKDF2'},false,['deriveBits']);const pbkdf2Bits=await crypto.subtle.deriveBits({name:'PBKDF2',salt,iterations:500000,hash:'SHA-256'},pbkdf2Key,512);const hkdfKey=await crypto.subtle.importKey('raw',pbkdf2Bits,{name:'HKDF'},false,['deriveBits']);const aesBits=await crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-256',salt:new Uint8Array(0),info:utils.stringToArrayBuffer('AES-CTR')},hkdfKey,256);const chachaBits=await 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',aesBits,{name:'AES-CTR'},false,['encrypt','decrypt']);return{aesCtrKey,chachaKey:new Uint8Array(chachaBits)};}catch(error){console.error('密钥派生失败:',error);throw new Error('密钥派生过程发生错误,请检查输入参数');}},async encryptAESCTR(plaintext,key){try{const counter=utils.generateRandomBytes(16);const encrypted=await crypto.subtle.encrypt({name:'AES-CTR',counter,length:128},key,plaintext);return new Uint8Array([...counter,...new Uint8Array(encrypted)]).buffer;}catch(error){console.error('AES-CTR加密失败:',error);throw new Error('AES-CTR加密过程发生错误');}},async decryptAESCTR(ciphertext,key){try{const counter=ciphertext.slice(0,16);const encrypted=ciphertext.slice(16);return crypto.subtle.decrypt({name:'AES-CTR',counter,length:128},key,encrypted);}catch(error){console.error('AES-CTR解密失败:',error);throw new Error('AES-CTR解密过程发生错误,可能密钥不正确');}},async encryptChaCha20Poly1305(plaintext,key){try{const nonce=sodium.randombytes_buf(12);const encrypted=sodium.crypto_aead_chacha20poly1305_ietf_encrypt(new Uint8Array(plaintext),null,null,nonce,key);return new Uint8Array([...nonce,...encrypted]).buffer;}catch(error){console.error('ChaCha20加密失败:',error);throw new Error('ChaCha20加密过程发生错误');}},async decryptChaCha20Poly1305(ciphertext,key){try{const nonce=ciphertext.slice(0,12);const encrypted=ciphertext.slice(12);return sodium.crypto_aead_chacha20poly1305_ietf_decrypt(null,new Uint8Array(encrypted),null,new Uint8Array(nonce),key);}catch(error){console.error('ChaCha20解密失败:',error);throw new Error('ChaCha20解密过程发生错误,可能密钥或数据已损坏');}},isBase64(str){return/^[A-Za-z0-9+/=]+$/.test(str);},async encrypt(plaintext,password,isBase64Enabled=true){try{const salt=utils.generateRandomBytes(16);const{aesCtrKey,chachaKey}=await this.deriveKey(password,salt);const compressed=await utils.compressData(utils.stringToArrayBuffer(plaintext));const aesCtrEncrypted=await this.encryptAESCTR(compressed,aesCtrKey);const chachaEncrypted=await this.encryptChaCha20Poly1305(aesCtrEncrypted,chachaKey);const combined=new Uint8Array([...salt,...new Uint8Array(chachaEncrypted)]);let result=utils.arrayBufferToBase64(combined.buffer);if(!isBase64Enabled){result=mappingMode1.base64ToChinese(result);} -return result;}catch(error){console.error('数据加密失败:',error);throw new Error(`加密过程发生错误: ${error.message}`);}},async decrypt(ciphertext,password){try{let processedCiphertext=ciphertext;if(!this.isBase64(ciphertext)){processedCiphertext=mappingMode1.chineseToBase64(ciphertext);} -const ciphertextBuffer=utils.base64ToArrayBuffer(processedCiphertext);const salt=ciphertextBuffer.slice(0,16);const encryptedData=ciphertextBuffer.slice(16);const{aesCtrKey,chachaKey}=await this.deriveKey(password,salt);const chachaDecrypted=await this.decryptChaCha20Poly1305(encryptedData,chachaKey);const aesCtrDecrypted=await this.decryptAESCTR(chachaDecrypted,aesCtrKey);return utils.arrayBufferToString(await utils.decompressData(aesCtrDecrypted));}catch(error){console.error('数据解密失败:',error);if(error.message.includes('decryption failed')){throw new Error('解密失败:可能密码错误或数据已损坏');}else if(error.message.includes('key derivation')){throw new Error('密钥派生失败:请检查密码长度和复杂度');}else{throw new Error(`解密过程发生错误: ${error.message}`);}}}}; \ No newline at end of file +const encryptionMethod1={async deriveKey(password,salt){try{const passwordBuffer=utils.stringToArrayBuffer(password);const pbkdf2Key=await crypto.subtle.importKey('raw',passwordBuffer,{name: 'PBKDF2'},false,['deriveBits']);const pbkdf2Bits=await crypto.subtle.deriveBits({name: 'PBKDF2',salt,iterations: 500000,hash: 'SHA-256'},pbkdf2Key,512);const hkdfKey=await crypto.subtle.importKey('raw',pbkdf2Bits,{name: 'HKDF'},false,['deriveBits']);const aesBits=await crypto.subtle.deriveBits({name: 'HKDF',hash: 'SHA-256',salt: new Uint8Array(0),info: utils.stringToArrayBuffer('AES-CTR')},hkdfKey,256);const chachaBits=await 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',aesBits,{name: 'AES-CTR'},false,['encrypt','decrypt']);return{aesCtrKey,chachaKey: new Uint8Array(chachaBits)};}catch(error){console.error('密钥派生失败:',error);throw new Error('密钥派生过程发生错误,请检查输入参数');}},async encryptAESCTR(plaintext,key){try{const counter=utils.generateRandomBytes(16);const encrypted=await crypto.subtle.encrypt({name: 'AES-CTR',counter,length: 128},key,plaintext);return new Uint8Array([...counter,...new Uint8Array(encrypted)]).buffer;}catch(error){console.error('AES-CTR加密失败:',error);throw new Error('AES-CTR加密过程发生错误');}},async decryptAESCTR(ciphertext,key){try{const counter=ciphertext.slice(0,16);const encrypted=ciphertext.slice(16);return crypto.subtle.decrypt({name: 'AES-CTR',counter,length: 128},key,encrypted);}catch(error){console.error('AES-CTR解密失败:',error);throw new Error('AES-CTR解密过程发生错误,可能密钥不正确');}},async encryptChaCha20Poly1305(plaintext,key){try{const nonce=sodium.randombytes_buf(12);const encrypted=sodium.crypto_aead_chacha20poly1305_ietf_encrypt(new Uint8Array(plaintext),null,null,nonce,key);return new Uint8Array([...nonce,...encrypted]).buffer;}catch(error){console.error('ChaCha20加密失败:',error);throw new Error('ChaCha20加密过程发生错误');}},async decryptChaCha20Poly1305(ciphertext,key){try{const nonce=ciphertext.slice(0,12);const encrypted=ciphertext.slice(12);return sodium.crypto_aead_chacha20poly1305_ietf_decrypt(null,new Uint8Array(encrypted),null,new Uint8Array(nonce),key);}catch(error){console.error('ChaCha20解密失败:',error);throw new Error('ChaCha20解密过程发生错误,可能密钥或数据已损坏');}},isBase64(str){return/^[A-Za-z0-9+/=]+$/.test(str);},async encrypt(plaintext,password,outputMode='base64'){try{const salt=utils.generateRandomBytes(16);const{aesCtrKey,chachaKey}=await this.deriveKey(password,salt);const compressed=await utils.compressData(utils.stringToArrayBuffer(plaintext));const aesCtrEncrypted=await this.encryptAESCTR(compressed,aesCtrKey);const chachaEncrypted=await this.encryptChaCha20Poly1305(aesCtrEncrypted,chachaKey);const combined=new Uint8Array([...salt,...new Uint8Array(chachaEncrypted)]);let result=utils.arrayBufferToBase64(combined.buffer);switch(outputMode){case 'chinese': result=mappingMode1.base64ToChinese(result);break;case 'emoji': result=mappingMode2.base64ToEmoji(result);break;}return result;}catch(error){console.error('数据加密失败:',error);throw new Error(`加密过程发生错误: ${error.message}`);}},async decrypt(ciphertext,password){try{const detectedMode=utils.detectCiphertextType(ciphertext);let processedCiphertext=ciphertext;if(detectedMode==='emoji'){processedCiphertext=mappingMode2.emojiToBase64(ciphertext);}else if(detectedMode==='chinese'){processedCiphertext=mappingMode1.chineseToBase64(ciphertext);}const ciphertextBuffer=utils.base64ToArrayBuffer(processedCiphertext);const salt=ciphertextBuffer.slice(0,16);const encryptedData=ciphertextBuffer.slice(16);const{aesCtrKey,chachaKey}=await this.deriveKey(password,salt);const chachaDecrypted=await this.decryptChaCha20Poly1305(encryptedData,chachaKey);const aesCtrDecrypted=await this.decryptAESCTR(chachaDecrypted,aesCtrKey);return utils.arrayBufferToString(await utils.decompressData(aesCtrDecrypted));}catch(error){console.error('数据解密失败:',error);if(error.message.includes('decryption failed')){throw new Error('解密失败:可能密码错误或数据已损坏');}else if(error.message.includes('key derivation')){throw new Error('密钥派生失败:请检查密码长度和复杂度');}else{throw new Error(`解密过程发生错误: ${error.message}`);}}}}; \ No newline at end of file diff --git a/js/mapping-mode-1.js b/js/mapping-mode-1.js index 9467b89..0411fcf 100644 --- a/js/mapping-mode-1.js +++ b/js/mapping-mode-1.js @@ -1,4 +1 @@ -const CharSets={"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(CharSets)){for(const chineseChar of chineseChars){combinedCharMap[chineseChar]=base64Char;}} -const mappingMode1={base64ToChinese:function(base64){let chinese='';for(let i=0;i [...b].length-[...a].length);const mappingMode2={base64ToEmoji:(base64)=>{return [...base64].reduce((emojiStr,char)=>{const charSet=CharSets2[char];return charSet ? emojiStr+charSet[Math.floor(Math.random()*charSet.length)] : emojiStr;},'');},emojiToBase64:(emoji)=>{const emojiArray=[...emoji];let base64='';let i=0;while(i < emojiArray.length){let matched=false;const maxCheckLength=Math.min(5,emojiArray.length-i);for(let len=maxCheckLength;len >=1;len--){const candidate=emojiArray.slice(i,i+len).join('');if(combinedCharMap2.hasOwnProperty(candidate)){base64+=combinedCharMap2[candidate];i+=len;matched=true;break;}}if(!matched){console.warn(`未找到匹配的emoji: ${emojiArray[i]}`);i++;}}return base64;},getEmojiLength:(emoji)=> [...emoji].length}; \ No newline at end of file diff --git a/js/utils.js b/js/utils.js index f4ea863..7ce98c6 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,6 +1 @@ -const DEFAULT_PASSWORD='a184f7b849ffffed24d266a30298c72ef2f5ad040db73bf37151fac767630728';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);} -return new Blob(chunks).arrayBuffer();} -const utils={arrayBufferToBase64(buffer){let binary='';const bytes=new Uint8Array(buffer);for(let i=0;i{console.error('复制失败:',err);showNotification('复制到剪贴板失败,请手动复制',false);});}}; \ No newline at end of file +const DEFAULT_PASSWORD='a184f7b849ffffed24d266a30298c72ef2f5ad040db73bf37151fac767630728';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);}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){if(/^[A-Za-z0-9+/=]+$/.test(ciphertext)){return 'base64';}const emojiRegex=/[\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;if(emojiRegex.test(ciphertext)){return 'emoji';}return 'chinese';}}; \ No newline at end of file diff --git a/js/web.js b/js/web.js index 035bf2e..1b0682b 100644 --- a/js/web.js +++ b/js/web.js @@ -1,4 +1,4 @@ -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=` +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=`
@@ -10,10 +10,4 @@ function showNotification(message,isSuccess=true){const notificationContainer=do
- `;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')};const isDarkMode=localStorage.theme==='dark'||(!('theme'in localStorage)&&window.matchMedia('(prefers-color-scheme: dark)').matches);if(isDarkMode){document.documentElement.classList.add('dark');elements.themeToggle.innerHTML='';} -const checkBrowserSupport=()=>{const requiredAPIs=[{api:window.crypto.subtle,message:'当前浏览器不支持Web Crypto API,无法使用加密功能'},{api:window.CompressionStream&&window.DecompressionStream,message:'当前浏览器不支持压缩/解压缩API,无法使用加密功能'}];for(const{api,message}of requiredAPIs){if(!api){showNotification(message,false);elements.encryptBtn.disabled=true;elements.decryptBtn.disabled=true;return false;}} -try{new CompressionStream('deflate');new DecompressionStream('deflate');return true;}catch(e){showNotification('当前浏览器不支持Deflate压缩算法,无法使用加密功能',false);elements.encryptBtn.disabled=true;elements.decryptBtn.disabled=true;return false;}};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=``;});let isBase64Enabled=false;elements.base64Toggle.addEventListener('click',()=>{isBase64Enabled=!isBase64Enabled;elements.base64Toggle.innerHTML=``;showNotification(`已切换到 ${isBase64Enabled ? 'Base64' : '中文'} 模式`);});elements.pasteBtn.addEventListener('click',async()=>{try{const clipboardText=await navigator.clipboard.readText();elements.textInput.value=clipboardText;showNotification('已从剪贴板粘贴文本');}catch(error){showNotification('无法从剪贴板粘贴文本',false);}});const handleEncryptionDecryption=async(action,inputText,password,isDefaultPassword)=>{try{elements.resultStatus.textContent=`${action}中......`;elements.resultStatus.className='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';elements.resultStatus.classList.remove('hidden');const result=action==='加密'?await encryptionMethod1.encrypt(inputText,password,isBase64Enabled):await encryptionMethod1.decrypt(inputText,password,isBase64Enabled);elements.resultOutput.value=result;elements.resultStatus.textContent=`${action}成功`;elements.resultStatus.className='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='';elements.resultStatus.textContent=`${action}失败`;elements.resultStatus.className='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';elements.resultStatus.classList.remove('hidden');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;} -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');localStorage.theme=isDark?'dark':'light';elements.themeToggle.innerHTML=``;const notifications=document.querySelectorAll('.fixed.right-4.transition-all.duration-300.ease-in-out.z-50');notifications.forEach(notification=>{updateNotificationStyle(notification);});});}); \ No newline at end of file + `;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')};const isDarkMode=localStorage.theme==='dark'||(!('theme' in localStorage)&&window.matchMedia('(prefers-color-scheme: dark)').matches);if(isDarkMode){document.documentElement.classList.add('dark');elements.themeToggle.innerHTML='';}const checkBrowserSupport=()=>{const requiredAPIs=[{api:window.crypto.subtle,message:'当前浏览器不支持Web Crypto API,无法使用加密功能'},{api:window.CompressionStream&&window.DecompressionStream,message:'当前浏览器不支持压缩/解压缩API,无法使用加密功能'}];for(const{api,message}of requiredAPIs){if(!api){showNotification(message,false);elements.encryptBtn.disabled=true;elements.decryptBtn.disabled=true;return false;}}try{new CompressionStream('deflate');new DecompressionStream('deflate');return true;}catch(e){showNotification('当前浏览器不支持Deflate压缩算法,无法使用加密功能',false);elements.encryptBtn.disabled=true;elements.decryptBtn.disabled=true;return false;}};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=``;});let outputMode='chinese';elements.base64Toggle.addEventListener('click',()=>{switch(outputMode){case 'chinese':outputMode='base64';break;case 'base64':outputMode='emoji';break;case 'emoji':outputMode='chinese';break;}const icons={chinese:'language',base64:'code',emoji:'smile'};elements.base64Toggle.innerHTML=``;showNotification(`已切换到 ${outputMode === 'chinese' ? '中文' : outputMode === 'base64' ? 'Base64' : 'Emoji'} 模式`);});elements.pasteBtn.addEventListener('click',async()=>{try{const clipboardText=await navigator.clipboard.readText();elements.textInput.value=clipboardText;showNotification('已从剪贴板粘贴文本');}catch(error){showNotification('无法从剪贴板粘贴文本',false);}});const handleEncryptionDecryption=async(action,inputText,password,isDefaultPassword)=>{try{elements.resultStatus.textContent=`${action}中......`;elements.resultStatus.className='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';elements.resultStatus.classList.remove('hidden');const result=action==='加密' ? await encryptionMethod1.encrypt(inputText,password,outputMode):await encryptionMethod1.decrypt(inputText,password);elements.resultOutput.value=result;elements.resultStatus.textContent=`${action}成功`;elements.resultStatus.className='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='';elements.resultStatus.textContent=`${action}失败`;elements.resultStatus.className='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';elements.resultStatus.classList.remove('hidden');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);showNotification(`已识别密文类型: ${detectedMode === 'chinese' ? '中文' : detectedMode === 'base64' ? 'Base64' : 'Emoji'}`);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');localStorage.theme=isDark ? 'dark':'light';elements.themeToggle.innerHTML=``;const notifications=document.querySelectorAll('.fixed.right-4.transition-all.duration-300.ease-in-out.z-50');notifications.forEach(notification=>{updateNotificationStyle(notification);});});}); \ No newline at end of file