离线PWA数据加密实战:SJCL密钥管理与AES-GCM落地
1. 离线PWA为什么要上加密先聊聊安全底色做了几年前端踩过不少离线存储的坑我越来越确信一件事Service Worker Cache Storage IndexedDB 这套组合虽然好用但默认情况下它一点都不安全。很多人有个误区觉得“PWA 必须跑在 HTTPS 下数据应该挺安全的”。但 HTTPS 保护的是传输链路一旦数据落地到用户设备上——不管是浏览器的 IndexedDB、Cache Storage还是 localStorage——它就完全暴露在本地环境里。只要设备本身不可信比如共享电脑、被植入恶意软件的机器任何能打开 DevTools 的人、任何能读取磁盘文件的进程都能把你的数据翻个底朝天。这个场景在离线优先的应用里尤其致命。想象一下你做了一个离线笔记应用、离线报销工具、离线问诊记录系统用户在有网的时候会把敏感数据同步到本地然后在没网的环境下继续使用。这些数据长期沉睡在设备上等于在用户硬盘里埋了一堆明文地雷。所以正确的姿势是在落盘之前先加密在读取之后再解密。加密的活儿可以交给 SJCLStanford JavaScript Crypto Library——这是斯坦福团队维护的一个纯 JavaScript 加密库设计目标就是能在浏览器里高效跑密码学运算而且它天然适配 PWA 的场景体积可控、无原生依赖、支持树摇。这篇文章不会讲一堆空洞的架构概念而是从实际问题出发拆解如何用 SJCL 给离线 PWA 的本地数据做一套端到端的加密方案。包括为什么 IndexedDB 里的数据必须加密、SJCL 相比 Web Crypto API 的优势在哪、密钥怎么管理才不会让加密形同虚设、口令派生参数怎么配才安全、以及我自己实测踩过的几个大坑。不管你是正在做离线应用、想把敏感数据保护起来还是单纯对浏览器端加密方案感兴趣这篇都能给你一套能直接落地的思路。2. 方案选型为什么是 SJCL而不是“裸用” Web Crypto2.1 Web Crypto API 的先天短板现代浏览器其实自带了一套原生加密接口——Web Crypto API能跑 AES、RSA、SHA 这些算法性能也比纯 JS 实现好得多。那为什么我还要用 SJCL答案很简单Web Crypto API 的异步模型和 PWA 离线缓存场景存在明显错配。Web Crypto API 的几乎所有加密操作都是异步的这意味着你得把加密逻辑写成一串 Promise 链或者 await 表达式。这本身不是大问题但在离线 PWA 里数据读写往往要穿透多层缓存逻辑比如先查内存、再查 IndexedDB、再回源每层都插入一个异步加密/解密操作会让代码复杂度和排错成本翻倍。更麻烦的是Web Crypto API 的密钥导入导出虽然安全但接口设计偏向底层。你要自己管理 CryptoKey 对象的存储、用途标记usages、算法参数如 AES-GCM 的 iv 长度、tag 长度这些细节一旦配错要么加密失败要么留下安全漏洞。还有一点Web Crypto API 依赖浏览器环境。如果你想在 Web Worker 里做加密或者未来想把同一套加密逻辑复用到 Node.js 环境的构建脚本里原生接口会出现环境和实现差异。SJCL 是纯 JavaScript同一份代码在浏览器、Worker、Node 里都能跑没有兼容性分裂。2.2 SJCL 的几点独特优势SJCL 最早由斯坦福的密码学团队维护多年没有爆出过严重的安全漏洞社区认可度是够的。而具体到离线 PWA 场景它有四个让我觉得“真香”的地方内置 AES-GCM 模式认证加密一步到位。GCM 模式在加密的同时生成认证标签能防篡改。很多人不知道如果你只用 CBC 模式早期很多教程的标配数据被篡改是发现不了的。SJCL 把 GCM 封装成开箱即用的 API避免了“加密不等于防篡改”的认知陷阱。口令密钥派生内置 PBKDF2。离线场景里密钥通常来源于用户口令而用户口令的熵通常不够。SJCL 内置 PBKDF2 实现可以直接把弱口令拉伸成高强度的 AES 密钥省去自己拼装算法工具的麻烦。可插拔随机源。SJCL 在浏览器里会用crypto.getRandomValues收集熵确保每次加密的 IV 都足够随机。如果自己用Math.random()生成 IV加密方案基本等于白搭——但 SJCL 帮你把这个坑填平了。体积友好。压缩后大约 30KB 左右配合 PWA 的缓存策略对首屏加载影响几乎可以忽略。相比引入一套 OpenPGP.js几百 KBSJCL 算是轻量中的轻量。2.3 什么时候应该选 Web Crypto我不搞非黑即白。如果满足下面这些条件你确实可以直接用 Web Crypto你的应用是纯在线场景不需要复杂的离线数据同步逻辑加密操作频率不高异步带来的复杂度可以接受你的代码只在现代浏览器里跑不打算兼容任何旧内核团队里有人对密钥管理、算法参数足够熟练能自己处理底层细节。但如果目标是“离线优先 敏感数据 跨环境复用”SJCL 的封装粒度确实更省心。注意SJCL 目前维护频率不算高频使用前一定要锁定版本不要盲目升级到破坏性 API 的新版本。我用的是 1.0.8稳定性经过多年验证没出过幺蛾子。3. 加密方案的整体设计密钥、数据和流程的三角关系3.1 本地加密不是“把数据 XOR 一下”的事构建离线加密方案最容易犯的错误是把注意力全放在“选哪个加密算法”上却忽略了全局设计。其实一个加密系统的安全性主要取决于密钥管理而不是算法本身。你就算用了 AES-256-GCM如果密钥写死在代码里那和用 ROT13 没本质区别——攻击者只要把 JS 文件拖进格式化工具三分钟就能把密钥掏出来。所以离线 PWA 的加密方案至少要拆成三层来考虑数据层用什么算法加密实际数据明文 JSON/Blob → 密文密钥层数据加密密钥Data Encryption Key, DEK怎么生成、怎么保存身份层用户口令怎么转化成能解锁密钥的东西。这三层之间的关系可以用一个很直观的比喻来理解你把贵重物品数据锁进保险箱DEK 加密保险箱的钥匙DEK不能挂在保险箱外面所以再用一个主密码用户口令派生出的密钥把它锁进另一个保险箱。只要主密码不泄露整体就是安全的。3.2 我推荐的方案主密钥包裹Key Wrapping模式我实践下来最稳的离线加密模式是主密钥包裹 数据密钥随机生成。具体拆解用户首次设置应用时输入一个主密码口令Passphrase用 SJCL 的 PBKDF2 把口令拉伸生成一个 256 位的主密钥Master Key随机生成一个 256 位的数据密钥DEK专门用来加密实际的业务数据用主密钥加密 DEK把加密后的 DEK即 wrapped key存储到 IndexedDB 里业务数据一律用 DEK 加密后落盘DEK 本身以密文形态保存不落明文。为什么要加一道“数据密钥”而不直接用主密钥加密业务数据因为业务数据量大、读写频繁。如果直接用主密钥加密所有数据每次用户修改主密码你就要重新加密全量数据——这个成本在离线大缓存场景下根本无法接受。而 key wrapping 模式下修改口令只影响主密钥只需要用新主密钥重新包裹一次 DEK 就行全量数据完全不动。这就是密码学里很经典的“分级密钥”思想用高频变化的口令保护低频变化的 DEK再用 DEK 保护数据。分层之后每一层的更换成本都降到最低。3.3 存储结构与明文元数据在 IndexedDB 里我推荐单独建一个 key-value 对象仓库object store专门用来存密钥包裹数据字段类型说明idstring固定值如master_key_wrapwrapped_dekstring被主密钥加密后的 DEKbase64kdf_saltstringPBKDF2 使用的盐值base64kdf_iterationsnumberPBKDF2 迭代次数cipher_namestring加密算法标识如aes-256-gcmversionnumber密钥方案版本号便于未来迁移注意细节盐值salt必须随机生成并且需要和密文分开存放。这里说的“分开”不是放到两个数据库而是说不能写死在代码里。因为盐值的作用就是防止预计算彩虹表攻击如果所有用户共用同一个盐口令破解难度会大幅下降。每个用户、每次口令变更都应该生成独立的盐。而业务数据的存储结构建议统一封装成一个带 version 的 JSON{ v: 1, alg: AES-GCM, iv: base64 iv, ct: base64 ciphertext }这样无论未来算法升级还是参数调整都能通过 version 和 alg 字段做兼容迁移。4. 核心实现SJCL 加解密与密钥管理实战4.1 SJCL 的基本 API 形态SJCL 的加解密接口非常简洁核心是sjcl.encrypt和sjcl.decrypt。不过这两个函数是“高级封装”它们内部会自动生成 IV、自动做密钥派生适合快速上手。但如果你要做 key wrapping 这种分步操作建议用sjcl.json子模块和底层密码原语。先看最简单的用法适合非敏感场景或原型验证import sjcl from sjcl; // 加密 const plaintext 这是一段需要保护的离线数据; const password 用户主密码; // sjcl.encrypt 返回一个 JSON 字符串包含 iv、salt、ct、迭代次数等 const ciphertext sjcl.encrypt(password, plaintext); // 解密 const decrypted sjcl.decrypt(password, ciphertext); console.log(decrypted); // 原文这个高级接口把盐、IV、迭代次数全部自动生成并塞进输出 JSON 里对快速原型来说非常方便。但进入生产级方案我建议进一步拆开使用sjcl.misc.pbkdf2sjcl.cipher.aessjcl.mode.gcm组合来做。因为sjcl.encrypt默认使用口令直接作为密钥来源而在 key wrapping 场景下我们已经在用 PBKDF2 派生主密钥了没必要让内部再派生一次。4.2 分步实现口令派生、密钥包裹、数据加密下面是我在项目里沉淀下来的一套核心工具函数直接可用。为保持简洁这里用 ES Module 导入方式构建工具用 Vite 或 Webpack 都行。import sjcl from sjcl; const KDF_ITERATIONS 100000; // 后面解释为什么是这个数 const KEY_BITS 256; // 工具随机生成 base64 字符串 function randomBase64(bytes 16) { const words sjcl.random.randomWords(Math.ceil(bytes / 4)); return sjcl.codec.base64.fromBits(words); } // 1. 从用户口令派生主密钥 function deriveMasterKey(password, saltB64, iterations KDF_ITERATIONS) { const saltBits sjcl.codec.base64.toBits(saltB64); // 用 PBKDF2-HMAC-SHA256 拉伸口令输出 256 位密钥 const derivedKey sjcl.misc.pbkdf2(password, saltBits, iterations, KEY_BITS); return derivedKey; // BitArray } // 2. 生成数据密钥DEK function generateDEK() { return sjcl.random.randomWords(KEY_BITS / 32); // 8 个 32 位字 256 位 } // 3. 用主密钥包裹 DEK function wrapDEK(masterKeyBits, dekBits) { // 生成随机 IV96 位GCM 推荐值 const iv sjcl.random.randomWords(3); // 96 bit const aes new sjcl.cipher.aes(masterKeyBits); // GCM 模式加密额外的认证数据留空 const ciphertextBits sjcl.mode.gcm.encrypt(aes, dekBits, iv, [], 128); return { iv: sjcl.codec.base64.fromBits(iv), wrapped: sjcl.codec.base64.fromBits(ciphertextBits), }; } // 4. 用主密钥解包 DEK function unwrapDEK(masterKeyBits, wrappedB64, ivB64) { const wrappedBits sjcl.codec.base64.toBits(wrappedB64); const ivBits sjcl.codec.base64.toBits(ivB64); const aes new sjcl.cipher.aes(masterKeyBits); const dekBits sjcl.mode.gcm.decrypt(aes, wrappedBits, iv, [], 128); return dekBits; } // 5. 用 DEK 加密业务数据 function encryptData(dekBits, plaintext) { // 明文先转 UTF-8 位数组 const plaintextBits sjcl.codec.utf8String.toBits(plaintext); const iv sjcl.random.randomWords(3); const aes new sjcl.cipher.aes(dekBits); const ciphertextBits sjcl.mode.gcm.encrypt(aes, plaintextBits, iv, [], 128); return { iv: sjcl.codec.base64.fromBits(iv), ct: sjcl.codec.base64.fromBits(ciphertextBits), alg: AES-GCM, v: 1, }; } // 6. 用 DEK 解密业务数据 function decryptData(dekBits, encData) { const ivBits sjcl.codec.base64.toBits(encData.iv); const ctBits sjcl.codec.base64.toBits(encData.ct); const aes new sjcl.cipher.aes(dekBits); const plaintextBits sjcl.mode.gcm.decrypt(aes, ctBits, ivBits, [], 128); return sjcl.codec.utf8String.fromBits(plaintextBits); }代码不复杂但有几个关键细节我必须展开讲不然很容易出问题。细节一为什么单独拆出generateDEK而不是复用主密钥如果用户用的是低熵口令比如 6 位数字密码即使经过 PBKDF2 拉伸主密钥的熵依然不够。此时如果直接用主密钥加密业务数据攻击者可以用离线字典攻击逐条尝试口令解出数据。而 DEK 是随机生成的 256 位密钥熵极高即使业务数据密文被拖走攻击者也无法绕过主密钥直接破解 DEK——他能获得的只是被包裹的 DEK 密文没有任何暴力破解的捷径。细节二GCM 模式的 tag 长度设多少代码里sjcl.mode.gcm.encrypt最后一个参数128代表认证标签GCM tag的长度。SJCL 支持 128 位 tag这是 GCM 的标准安全配置不推荐为了省空间改成 96 位或更短。因为 tag 承担着完整性校验的职责太短会提高伪造概率。细节三IV 为什么是 96 位GCM 的 IV 标准推荐值就是 96 位12 字节这是 NIST 的推荐。小于这个值需要做特殊处理大于这个值会降低内部计数器效率。SJCL 的randomWords(3)刚好生成 3 个 32 位字即 96 位正合适。4.3 密钥变更流程改密码不该全量重加密用户改主密码时如果走“全量重加密”的老路体验会非常糟糕。想想一个离线知识库应用用户存了几百篇笔记一篇笔记算 10KB也有几 MB 数据。全量重加密意味着户要等上好几秒甚至更久期间还不能关闭页面。key wrapping 方案把改密成本降到了极致用户输入旧密码和新密码用旧密码派生旧主密钥解包出 DEK用新密码派生新主密钥重新包裹 DEK只更新 IndexedDB 里的 wrapped_dek、kdf_salt、kdf_iterations 字段完事业务数据一个字节都不用动。整个过程只涉及两次 PBKDF2 派生和一次 GCM 加解密处理时间在一百毫秒以内。用户几乎无感知。4.4 会话密钥缓存避免频繁派生PBKDF2 迭代 10 万次每次调用大约需要几十到几百毫秒取决于设备性能。如果在应用启动时、每次数据读写时都做一次派生体验会很糟糕。所以我在实践中会在内存/会话存储里缓存派生的主密钥和 DEK。具体做法用户在解锁页面输入密码后派生主密钥、解包 DEK然后存到一个模块级变量里只要当前标签页会话存活后续加解密都直接用内存里的 DEK不再走 PBKDF2用户主动锁定、切换账号、或标签页关闭时清除内存密钥。这个设计类似手机上的“解锁后保持会话”机制。安全性上有细微牺牲内存中的密钥可被同进程的恶意代码窃取但任何端到端加密方案都躲不开这个权衡——只要在浏览器里运行进程内的恶意注入就无解。5. 实操避坑迭代次数、随机源与兼容性5.1 PBKDF2 迭代次数怎么定别拍脑袋很多教程把迭代次数写成 10000这在今天是远远不够的。PBKDF2 的迭代次数直接决定“离线字典攻击的成本”。假设攻击者拿到了一堆密文他只需要做一件事把每个常见口令跑一遍 PBKDF2然后用得到的密钥去尝试解包 DEK看是否能得到有效的 JSON 结构。迭代次数越高每次尝试消耗的时间越长。2017 年的 OWASP 建议是 120000 次2023 年之后建议 600000 次以上。但我实测下来600000 次 PBKDF2-HMAC-SHA256 在普通手机浏览器上可能要耗 1-2 秒这个体验很差。我的做法是动态校准在用户首次设置密码时先跑一个快速 benchmark——以 1000 次迭代试跑测出单次迭代耗时然后反推目标时间500ms对应的迭代次数并把它写进存储里。简化版逻辑async function calibrateIterations(targetMs 500) { const sampleIterations 1000; const salt sjcl.random.randomWords(4); const start performance.now(); sjcl.misc.pbkdf2(benchmark, salt, sampleIterations, 256); const elapsed performance.now() - start; const estimated Math.floor(sampleIterations * targetMs / elapsed); // 上下限保护 return Math.min(600000, Math.max(100000, estimated)); }这样在慢设备上自动降低迭代次数保证可用性在快设备上自动提高迭代次数保证安全取一个动态平衡。如果你不想做动态校准固定 100000-300000 次之间也是一个可以接受的范围但一定别低于 100000。5.2 SJCL 随机源的坑不要在非安全上下文里用SJCL 的随机数生成器在浏览器环境里会调用window.crypto.getRandomValues来播种。但在非安全上下文HTTP 页面或 iframe里crypto对象可能不可用SJCL 会发出警告并使用内置的弱随机源。PWA 本身要求 HTTPS所以正常部署不会踩到这个坑。但如果是本地开发localhost 除外、内网测试环境、或者某些 WebView 容器里一定要提前检查if (!window.crypto || !window.crypto.getRandomValues) { // 直接给出错误提示拒绝继续初始化 throw new Error(当前环境不支持安全随机数生成无法初始化加密模块); }为什么我把这个看得这么重因为如果 SJCL 回退到弱随机源它生成的所有 IV、盐、DEK 都可能被预测。攻击者一旦掌握 IV 的生成规律GCM 模式的安全性会全面崩溃。加密系统里随机性就是生命线宁可让程序跑不起来也不能让它悄悄用弱随机。5.3 离线场景的缓存与加密顺序问题PWA 有个典型场景Service Worker 把资源缓存进 Cache Storage运行时把数据写进 IndexedDB。如果你把这两件事混在一起处理容易陷入“缓存的内容到底要不要加密”的困惑。我个人的约定静态资源JS/CSS/图片/HTML不加密。它们是公开代码和样式加密没有意义还会拖慢加载、破坏流式缓存运行时业务数据笔记、表单草稿、用户配置必须加密后再写入 IndexedDB敏感的配置项如 API Token、密钥材料走加密 IndexedDB或者干脆存在内存里不落盘。这个约定的核心原则是能公开的公开必须保护的绝不碰明文。静态资源走 Service Worker 预缓存即可业务数据统一走 SJCL 加解密层。5.4 版本迁移问题密码方案要留后门加密方案不是一成不变的。随着 SJCL 的安全参数推荐值提高或者未来要迁移到 Web Crypto 的 AES-GCM你需要让存储结构支持版本迁移。我在前面放的存储结构中已经预留了version和alg字段实际中怎么用第一种迁移场景升级 KDF 迭代次数。如果用户当前存储里的迭代次数是 50000你希望提升到 300000。做法是在用户下次输入密码解锁时检测到旧迭代次数低于新阈值自动用旧参数解包 DEK再用新参数重新包裹并更新存储。用户全程无感。第二种迁移场景更换加密算法。比如从 SJCL 的 AES-GCM 换成 Web Crypto 的原生 AES-GCM。这就需要先解密数据再用新算法重新加密。这个过程可以在后台逐条迁移但注意要给每条记录标注 version按版本选择解密路径。我见过不少项目上线之后发现加密参数配得不合理却因为没有设计版本字段而被迫全量重来。这真的是地基级的教训——哪怕是第一版代码也要把 version 带上成本几乎为零收益是长期的。6. 完整流程串联从用户解锁到离线读写讲了这么多拆解用一个完整流程把它们串起来。假设你做一个离线记事本 PWA核心流程如下。6.1 首次初始化用户输入一个主密码至少 8 位建议强制 12 位以上前端生成随机盐16 字节用动态校准的迭代次数跑 PBKDF2得到主密钥随机生成 DEK用主密钥包裹 DEK把盐、迭代次数、包裹后的 DEK、版本号写入 IndexedDB 的key_store表完成初始化进入加解密工具类初始化流程。6.2 解锁流程从 IndexedDB 读取盐、迭代次数、包裹 DEK用户输入密码跑 PBKDF2 得到主密钥用主密钥解包 DEK把 DEK 存入内存变量所有后续加解密直接复用如果解包失败GCM 认证失败提示“密码错误”而不是显示泛泛的“解密失败”——因为 GCM 的认证机制天然能判断密钥是否正确这一点比旧的 CBC 模式体验好太多。6.3 写入一条笔记调用encryptData(dekBits, JSON.stringify(note))得到密文对象把密文对象存入 IndexedDB 的notes表同步逻辑如果有把密文同步到远端远端只碰密文不碰明文。6.4 读取一条笔记从 IndexedDB 取出密文对象调用decryptData(dekBits, encData)得到明文 JSON渲染到 UI用完的明文变量及时赋值为null减少内存驻留时间。整个流程中SJCL 负责三件核心事PBKDF2 派生、AES-GCM 加解密、安全随机数生成。它不负责密钥的存取策略——那部分由你自己的分层设计决定。7. 常见问题速查我把踩过的坑都写在这7.1 解密时报错 “ccm: tag doesnt match”这个报错几乎每一个用过 SJCL 的人都会遇到。原因只有两种可能密钥不对。PBKDF2 派生用的盐、迭代次数、口令有一项不一致导致主密钥错误最终 GCM 认证标签校验失败密文被篡改。存储的 wrapped_dek 或业务密文在传输、写入过程中被改动。排查方法在密钥派生后打印主密钥的 SHA-256 哈希并与首次初始化时保存的校验值比对如果当时存过的话。我没存的话就直接解码存储里的数据检查 base64 字符串是否前后一致排除存储层的问题。7.2 SJCL 在低端手机上解锁慢低端手机的 PBKDF2 性能差距非常大。同一台安卓千元机10 万次迭代可能耗时 800ms而旗舰机只要 100ms。这直接影响用户对解锁界面的感知。我的经验是两层优化一是在初始化时做动态迭代次数校准目标时间控制在 300-500ms二是解锁流程加过渡动画先把输入焦点给到用户在等 PBKDF2 的过程中显示加载态。人机交互上500ms 以内的等待基本无感。7.3 密码变更后老数据解不开了九成原因是变更流程里用错了盐。很多人改密码时只更新了密码字段忘了盐也要一起更新。别忘了PBKDF2 的输入是口令盐两个变量任何一个变了派生出的密钥就完全不同。如果新密钥包裹 DEK 成功但读数据时依然用旧逻辑拿盐就会解密失败。正确做法在 4.3 已经写过改密码务必同时换盐并更新存储。我习惯在代码里用同一个事务transaction更新这三个字段保证原子性——万一中间步骤失败了要么全更新要么全不更新绝不会出现盐和密钥版本错乱的中间态。7.4 SJCL 压缩后体积怎么控制未压缩的 SJCL 大约 140KBgzip 后约 30KB。在 PWA 里你可以把它加入静态资源预缓存列表利用 Service Worker 实现“首次访问加载一次后续全部走缓存”的效果。如果还想进一步瘦身SJCL 支持按需引入子模块。例如只用 AES-GCM 的场景可以精简掉 RSA、ECC、CBC 等模块需要结合打包工具做 tree-shaking。具体做法是在构建配置中只import sjcl/core/sjcl和import sjcl/core/aes、import sjcl/core/gcm、import sjcl/core/pbkdf2、import sjcl/core/random。注意 SJCL 的模块依赖关系比较琐碎精简时务必在构建后跑一遍完整的加解密自测确认没有遗漏子模块。7.5 能不能把 DEK 直接存到 localStorage不能。localStorage 没有事务支持而且任何脚本都能同步读取。如果你把 DEK 明文存进 localStorage等于给所有 XSS 攻击留了一扇大门。DEK 要么存在于内存要么以密文形态存放在 IndexedDB 里绝不能明文存储。8. 总结外的心得这方案还能怎么演进这套 SJCL 加密方案我已经在几个离线项目中跑了快一年稳定性和安全性都没出过问题。最后分享几个后续演进方向供你参考。结合 Web Crypto 做混合架构。SJCL 负责密钥派生和密钥包裹Web Crypto 负责高频数据加解密。SJCL 派生密钥后把密钥导入成 CryptoKey 对象后续批量加密走原生的 AES-GCM性能和兼容性可以兼顾。SJCL 的跨环境优势也能继续发挥。给“忘记密码”做数据恢复通道。离线加密和“找回密码”天然冲突——没有主密钥就永远解不开数据。但可以设计一个恢复密钥Recovery Key在首次初始化时生成一段 96 位的随机撤销码用户抄下来存到安全的地方。撤销码本身不派生密钥而是作为加密 DEK 的另一个“主密钥”。这样用户忘了口令时输入撤销码也能解包 DEK。对你的 Service Worker 缓存策略做分级。静态资源走 stale-while-revalidate加密模块的 JS 文件建议走 cache-first避免因为缓存失效导致用户打开应用时重新下载 30KB 的加密库——这在弱网环境下会影响解锁速度。我就是因为一开始没注意这个细节被用户在高铁上吐槽过加载太慢。离线加密这件事方案本身不难难的是把每一个细节都想到位。SJCL 给你提供了可靠的密码学原语但密钥怎么管、盐怎么存、迭代次数怎么校、版本怎么迁这些工程问题最终还是得靠你自己想清楚。希望这篇实战总结能帮你少走几步弯路。

相关新闻

GMP计算机化系统验证管理规程:GAMP5分类与验证清单实操指南

GMP计算机化系统验证管理规程:GAMP5分类与验证清单实操指南

简介:这份资源是一套面向制药企业QA、设备工程、IT及验证相关岗位的GMP计算机化系统验证管理规程文档,适用于物料控制、实验室设备、生产工艺及公用设施等与GMP相关的计算机控制系统验证场景,帮助读者建立从用户需求到系统验收的全生命周期验…

2026/10/17 18:16:50 阅读更多 →
JADE盲源分离算法:MATLAB完整实现与核心原理详解

JADE盲源分离算法:MATLAB完整实现与核心原理详解

简介:JADE盲源分离算法是经典的高阶统计量独立成分分析方法,面向语音信号处理、阵列信号分离等应用场景的研究人员与工程师。该算法以源信号统计独立及非高斯性为前提,最多仅允许一个高斯源,通过四阶累积量联合近似对角化完成混合…

2026/10/17 18:15:49 阅读更多 →
AI赋能业务落地指南:场景筛选、评估集与成本模型

AI赋能业务落地指南:场景筛选、评估集与成本模型

这两年常被人问到同一个问题:2026年了,AI到底还能怎么赋能业务?问的人有做电商运营的,有维护内部系统的,也有刚带团队的负责人。说实话,两三年前我听到这类问题,还会认真讲一堆算法原理。现在再…

2026/10/17 18:15:49 阅读更多 →

最新新闻

HTML菜单源码实战:jQuery多级下拉与手风琴折叠组件开发

HTML菜单源码实战:jQuery多级下拉与手风琴折叠组件开发

简介:这份源码包面向前端初学者与需要快速搭建导航栏的开发者,汇集了多套美观实用的HTML菜单实现方案,涵盖纯CSS基础菜单到结合JavaScript与jQuery的交互式下拉菜单,可用于个人项目、企业官网或后台系统的导航模块开发。压缩包共3…

2026/10/17 19:17:34 阅读更多 →
效果图转HTML完整流程:测量、切图到像素级还原实战指南

效果图转HTML完整流程:测量、切图到像素级还原实战指南

做网页这一行,最常听到的一句话就是:“我这有张HTML网页制作效果图,你照这个做就行。”语气很轻松,但真正动手的人都知道,从一张静态的效果图到一个能上线运行的HTML页面,中间隔着的不是代码量,…

2026/10/17 19:17:34 阅读更多 →
HTML5绿色简历模板:语义化、响应式、数据驱动的毕设级前端资源

HTML5绿色简历模板:语义化、响应式、数据驱动的毕设级前端资源

简介:这是一款专为求职者设计的绿色主题HTML5响应式个人简历模板,面向前端初学者与正在准备求职作品集的开发者,解决在线简历缺乏专业性、适配性差及技术展示力不足的问题。资源包共50个文件,包含1个核心HTML入口页、4个CSS样式文…

2026/10/17 19:17:34 阅读更多 →
网站全站变灰技术方案:CSS滤镜实现、兼容性降级与性能优化

网站全站变灰技术方案:CSS滤镜实现、兼容性降级与性能优化

1. 从一行代码说起:网站变灰这件事到底在做什么每年到了特定纪念日,不少网站会主动把整站色调切换成黑白灰。这个动作看起来简单,但背后涉及的技术选型、兼容性处理、性能影响,以及“什么时候该灰、灰到什么程度、怎么灰才不出洋相…

2026/10/17 19:17:34 阅读更多 →
SAR入门实战:三点目标回波仿真与正侧视RD成像算法详解

SAR入门实战:三点目标回波仿真与正侧视RD成像算法详解

做SAR成像相关的学习或开发,最绕不开的一项基本功就是“回波仿真 成像算法验证”。我自己刚碰这行的时候,最头疼的就是公式堆了一屏,代码跑出来却是满屏噪点,根本不知道问题出在哪。后来反反复复折腾,发现把“SAR 三点…

2026/10/17 19:17:34 阅读更多 →
二维码活码系统源码解析:动态改址与扫码统计实现

二维码活码系统源码解析:动态改址与扫码统计实现

简介:这是一套面向开发者与信息化管理人员的二维码活码网站源码,用于实现动态码的生成、更新与后台管理,解决传统静态二维码内容无法修改、难以追踪扫描数据的问题。资源包共2000个文件,以761个php后端脚本、259个html页面、247个…

2026/10/17 19:16:34 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →