WebCrypto、PBKDF2 与 AES-GCM 技术栈
WebCrypto、PBKDF2 与 AES-GCM 技术栈
执行摘要
标准化的、浏览器原生的客户端加密(Client-Side Encryption, CSE)正成为现代 Web 应用程序开发中一个决定性的趋势,其背后驱动力源于日益严格的隐私法规和零信任安全模型的普及。本报告旨在验证一个核心论点:一个由 Web Crypto API 驱动,利用 PBKDF2 进行密钥派生,并采用 AES-GCM 进行认证加密的特定架构模式,是实现这一趋势的稳健、广泛采用且符合标准的实现方式。
该安全模型的核心在于,它通过在数据离开客户端之前对其进行加密,从而有效地保护了存储在服务器上的静态数据,使得服务器本身无法访问用户数据的明文内容。然而,本报告将深入剖析此模型与真正意义上的端到端加密(End-to-End Encryption, E2EE)通信系统之间的关键区别。后者通常提供更强的安全保证,例如前向保密性(Forward Secrecy)。
最终,本报告将提供战略性的、可操作的建议,强调安全密钥管理的至关重要性,正确选择加密参数的必要性,以及缓解客户端环境本身所面临威胁的紧迫性。对于旨在构建下一代安全、注重隐私的 Web 应用程序的架构师和技术领导者而言,理解这一技术栈的深度、广度和局限性是必不可少的。
1. 引言:向零信任 Web 架构的范式转变
Web 安全的演进
Web 安全架构的演进历程,是一部从边界防御到纵深防御,再到如今零信任模型的宏大叙事。最初,安全重心集中在网络边界,依赖防火墙和传输层安全协议(TLS)作为主要防线。这种模式假定网络内部是可信的,而外部是不可信的。然而,随着应用程序复杂性的增加和攻击手段的多样化,这种“城堡与护城河”式的模型暴露出其固有的脆弱性。一旦攻击者突破边界,内部系统和数据便门户大开。
为了应对这一挑战,安全范式转向了纵深防御,即在系统的多个层面部署安全控制。尽管这提高了攻击的成本,但其核心信任假设并未改变:服务器在处理和存储数据时,本质上是可信的。然而,一系列大规模的服务器端数据泄露事件,以及内部威胁的日益凸显,彻底动摇了这一信任基础。这催生了零信任(Zero Trust)架构的兴起,其核心原则是“从不信任,始终验证”。在 Web 应用领域,零信任意味着服务器本身被视为一个潜在的不可信主机,即便它承载着应用程序的逻辑。这一根本性的转变,是驱动客户端加密(CSE)成为主流趋势的根本原因。
明确核心论点
本报告的核心任务,是深入分析并验证一个日益清晰的技术趋势:在 Web 应用程序中,通过组合使用 W3C 的 Web Cryptography API、用于密钥派生的 PBKDF2 算法以及用于认证加密的 AES-GCM 模式,来实现强大的客户端加密存储。这一论点不仅是理论上的可能性,更是一种在实践中被广泛采纳的、成熟的架构模式 1。它直接回应了现代安全思想的两大支柱:“端侧加密”(即服务器对用户数据零知识)和“每条消息唯一密钥”(尽管后者需要在此基础架构上进行扩展实现)。
核心组件概览
构成这一安全架构的技术栈主要包含三个标准化的关键组件,它们各司其职,共同构建了一个完整的客户端加密流程:
Web Crypto API:作为 W3C 的官方推荐标准,它为浏览器提供了一套底层的、与平台无关的 JavaScript 加密操作接口 3。它使得开发者能够安全地执行哈希、签名、加密和解密等操作,而无需直接接触原始密钥材料,从而将强大的加密能力从服务器端或浏览器插件中解放出来,使其成为 Web 平台的原生功能。
PBKDF2 (Password-Based Key Derivation Function 2):该算法在整个体系中扮演着桥梁的角色。它依据 RFC 2898 标准,将用户提供的、通常熵值较低的密码,通过“密钥拉伸”(key stretching)过程,转换为一个高熵的、适合用作加密密钥的二进制密钥 5。这一过程极大地增加了对密码进行暴力破解的计算成本。
AES-GCM (Advanced Encryption Standard - Galois/Counter Mode):这是执行实际数据加密的主力。作为一种认证加密与关联数据(AEAD)模式,AES-GCM 在一次高效的操作中同时提供了数据的机密性(通过 AES 计数器模式)和完整性/真实性(通过 GHASH 算法)7。这确保了数据不仅无法被窃读,也无法在传输或存储过程中被篡改而不被发现。
客户端加密的兴起,并非单纯的技术潮流,而是对外部压力的直接回应。一方面,如欧盟《通用数据保护条例》(GDPR)等全球性数据隐私法规的出台,对数据泄露事件施加了严厉的惩罚,这为企业提供了强烈的商业动机去最小化服务器端数据泄露的“爆炸半径”。另一方面,层出不穷的大规模服务器端数据泄露事件,使得用户和企业对将敏感数据以明文形式存储在第三方服务器上的风险有了更清醒的认识。
在这种背景下,客户端加密提供了一个优雅的架构解决方案。通过确保服务器永远无法接触到用户数据的明文或用于解密的密钥 9,CSE 从根本上将这些数据移出了服务器端数据泄露的波及范围。即使服务器被完全攻破,攻击者获取的也只是一堆无法解密的密文。因此,采纳 CSE 架构,已从一个单纯的技术选择,演变为一种由风险管理、合规需求和品牌声誉驱动的战略决策。这也解释了为何像谷歌 10、微软 9 和 IBM 11 这样的行业巨头,都在其云服务和企业解决方案中大力投入并推广客户端加密框架。这标志着 Web 数据所有权和信任模型的一次根本性重构。
2. 基础层:W3C Web Cryptography API
Web Cryptography API 是实现浏览器原生客户端加密的基石。它由万维网联盟(W3C)标准化,旨在为 Web 应用程序提供一套底层的、安全的、与平台无关的加密原语接口,从而让开发者能够构建更安全的应用,而无需依赖第三方插件或非标准库 2。
架构概览
该 API 的设计精巧地采用了双接口模型,将功能进行了逻辑分离,以增强安全性和易用性:
Window.crypto:这是 API 的主入口点,挂载在全局 window 对象上。它提供了两个核心属性:一个是指向 SubtleCrypto 接口的 subtle 属性,另一个是用于生成加密学安全伪随机数的 getRandomValues() 方法 14。后者对于生成盐(salt)、初始化向量(IV)等加密参数至关重要。
SubtleCrypto:这是 API 的核心,所有实际的加密操作都在此接口上执行。其方法包括 encrypt()、decrypt()、sign()、verify()、digest() 以及用于密钥管理的 generateKey()、deriveKey()、importKey() 和 exportKey() 等 13。W3C 的规范制定者特意将其命名为 "Subtle"(意为“微妙的、难以捉摸的”),这是一个明确的警告,旨在提醒开发者这些底层加密原语极易被误用,任何细微的错误都可能导致整个安全体系的崩溃 16。
CryptoKey 对象:安全的核心抽象
API 的设计哲学围绕着一个中心抽象——CryptoKey 对象。这个对象并非密钥材料本身,而是一个指向浏览器内部安全存储的密钥材料的不透明句柄(opaque handle)13。这种设计是其安全模型的基石,它有效地防止了密钥的原始字节泄露到不安全的 JavaScript 运行环境中。
CryptoKey 对象具有几个关键属性,用于控制其行为和安全性:
type:定义密钥的类型,可以是 "secret"(用于对称加密)、"private"(用于非对称加密的私钥)或 "public"(用于非对称加密的公钥)。
extractable:一个至关重要的布尔标志。如果设置为 false,密钥材料将永远无法通过 exportKey() 或 wrapKey() 方法导出。这是实现“密钥不出域”安全原则的关键,应尽可能地将密钥设置为不可导出 18。
usages:一个字符串数组,明确规定了该密钥可以执行的操作。例如,["encrypt", "decrypt"] 表示密钥只能用于加密和解密,而不能用于签名。这种权限分离遵循了最小权限原则,限制了密钥被滥用的可能性。
安全上下文与约束
为了防范中间人(Man-in-the-Middle, MitM)攻击,Web Crypto API 的使用被严格限制在“安全上下文”(Secure Contexts)中,这在实践中通常意味着必须通过 HTTPS 协议提供服务 15。如果一个恶意攻击者能够拦截或篡改从服务器传输到客户端的 JavaScript 代码,他们就可以在代码调用 Web Crypto API 之前窃取用户的密码,或者注入逻辑来滥用 API。HTTPS 确保了应用代码在传输过程中的机密性和完整性,这是整个客户端加密安全模型得以成立的前提。
支持的原语与算法无关性
Web Crypto API 的一个核心设计原则是算法无关性。它定义了一套通用的接口,可以适用于多种不同的加密算法,而不是将 API 与特定的算法绑定 2。这为未来的密码学发展提供了灵活性。在规范中,每种支持的算法都有一个明确的注册名称。对于本报告所讨论的技术栈,规范明确注册了 PBKDF2 用于密钥派生(deriveKey 操作)和 AES-GCM 用于加密/解密(encrypt/decrypt 操作)2。
Web Crypto API 的出现,标志着 Web 安全能力的一次根本性飞跃。它通过将强大的加密功能从服务器端或依赖插件的解决方案(如过去的 Flash 或 Java Applets)迁移到标准化的、浏览器原生的特性中,从而实现了强加密的“民主化”。这一转变极大地降低了实现客户端加密的门槛,使其成为所有现代 Web 应用的现实选择。
然而,这种能力的转移也伴随着信任锚点的迁移。在传统的服务器端加密模型中,用户信任的是服务器的数据处理和存储实践。而在客户端加密模型中,信任的重心转移到了由服务器分发的应用代码的完整性上。尽管加密操作本身是在浏览器提供的安全环境中执行的,并且可能利用底层操作系统的加密库甚至硬件安全模块(HSM),但调用这些安全操作的 JavaScript 代码仍然来自 Web 服务器。
这意味着,一个被攻破的服务器可以向用户提供一个恶意版本的应用程序。这个恶意脚本可以在用户输入密码后、将其传递给 subtle.importKey() 之前就将其窃取,或者在密钥被创建后,如果密钥被错误地标记为 extractable,恶意脚本也可以将其导出并发送给攻击者 20。因此,虽然 Web Crypto API 本身提供了安全的加密原语,但整个 CSE 应用的安全性现在严重依赖于确保客户端代码完整性的机制。这包括使用子资源完整性(Subresource Integrity, SRI)来验证外部脚本的哈希值,以及部署严格的内容安全策略(Content Security Policy, CSP)来限制脚本的来源和行为。信任问题并未被消除,而是被转化为了一个关于代码来源和完整性的新问题。架构师在设计 CSE 系统时,必须将保护客户端代码的完整性置于与选择加密算法同等重要的地位。
3. 从人类记忆到加密密钥:PBKDF2 的角色
在客户端加密架构中,如何安全地从用户提供的密码生成一个强大的加密密钥,是整个安全链条中最关键也最脆弱的一环。用户的密码通常是可记忆的、熵值较低的字符串,直接用作加密密钥会使系统极易受到字典攻击和暴力破解。Password-Based Key Derivation Function 2 (PBKDF2) 正是为解决这一问题而设计的标准化算法。
RFC 2898 深度解析
PBKDF2 由 IETF 在 RFC 2898 中定义,其核心思想是通过一个计算密集型的“密钥拉伸”过程,将一个低熵的密码转换为一个高熵的、长度固定的派生密钥(Derived Key, DK)5。其函数原型可以表示为:
DK=PBKDF2(PRF,Password,Salt,c,dkLen)
每个参数都扮演着至关重要的角色:
PRF (Pseudorandom Function):伪随机函数,是 PBKDF2 内部迭代的核心。通常,它是一个基于哈希的消息认证码(HMAC),例如 HMAC-SHA256。
Password:用户输入的原始密码,是密钥派生的基础。
Salt:盐值。这是一个为每个用户独立生成的、长度足够(NIST 推荐至少 128 位)的加密学安全随机数 5。盐值的核心作用是抵御预计算攻击,如彩虹表。通过为每个密码添加一个唯一的盐,即使两个用户使用了相同的密码,他们最终存储的哈希值也是完全不同的,这迫使攻击者必须对每个用户的密码进行独立的破解尝试 5。
c (Iteration Count):迭代次数。这是 PBKDF2 实现密钥拉伸的关键参数。算法会重复执行 PRF 运算 c 次。每一次迭代都会增加计算派生密钥所需的时间。通过设定一个足够大的迭代次数(例如,数十万次或更多),可以显著增加攻击者进行单次密码猜测的计算成本,从而有效延缓暴力破解的速度 5。随着计算硬件性能的提升,这个值需要定期评估和增加。
dkLen (Derived Key Length):期望的派生密钥长度,以字节为单位。这个长度取决于后续加密算法的需求,例如,对于 AES-256,dkLen 将是 32(即 256 位)。
在 Web Crypto API 中的实现
Web Crypto API 通过 subtle.deriveKey() 方法原生支持 PBKDF2。其标准实现流程如下:
导入密码:首先,不能直接将密码字符串作为密钥使用。必须通过 subtle.importKey() 方法,将密码的 UTF-8 编码字节作为原始数据("raw" 格式)导入,生成一个 CryptoKey 对象。这个 CryptoKey 的 usages 必须包含 "deriveKey",并且应被标记为不可导出(extractable: false)15。
派生密钥:然后,调用 subtle.deriveKey()。
第一个参数是算法对象,指定 name: "PBKDF2",并提供 salt(一个 Uint8Array)、iterations(一个数字)和 hash(例如 "SHA-256")。
第二个参数是上一步从密码导入的 baseKey。
第三个参数 derivedKeyType 指定了最终派生出的密钥将用于何种算法,例如 { name: "AES-GCM", length: 256 }。
最后两个参数是新密钥的 extractable 标志和 keyUsages 数组。
安全性分析与局限性
PBKDF2 作为一个成熟且广泛部署的标准,为基于密码的加密提供了坚实的安全基础。然而,它的设计也存在固有的局限性。PBKDF2 的计算成本主要依赖于 CPU 的处理能力,其内存消耗非常小。这一特性使其在面对专门为哈希计算优化的硬件,如图形处理单元(GPU)和专用集成电路(ASIC)时,防御效果会大打折扣 5。攻击者可以通过大规模并行计算,显著加速破解过程。
对比分析:内存困难型函数的兴起
为了应对 PBKDF2 的这一弱点,密码学界发展出了“内存困难型”(memory-hard)的密钥派生函数。这类函数的代表是 Argon2,它是 2015 年密码哈希竞赛(Password Hashing Competition)的获胜者。
内存困难型函数的核心思想是,在计算过程中不仅需要大量的 CPU 时间,还需要消耗大量的内存资源。攻击者如果想通过并行计算来加速破解,就必须为每个并行任务都配备相应的大量内存,这使得使用 GPU 或 ASIC 进行大规模并行攻击的成本急剧增加,甚至变得不可行。NIST 在其最新的数字身份指南(如 SP 800-63B)中,也已经开始承认并推荐使用如 Argon2 这样的现代哈希函数 26。
PBKDF2 被正式纳入 Web Crypto API 规范,这巩固了它作为 Web 应用程序中普遍可用的基线密钥派生函数的地位 2。对于任何希望实现客户端加密的开发者来说,PBKDF2 是最直接、最兼容的选择,因为它在所有遵循标准的现代浏览器中都有原生支持。
然而,权威的安全分析指出,PBKDF2 抵御现代、资金雄厚的攻击者的能力正在减弱,因为它并非内存困难型函数 5。与此同时,NIST 等标准机构和安全社区的最佳实践越来越倾向于推荐使用内存困难型的 Argon2 26。
这就为架构师带来了一个战略性的困境。当前,Argon2 并非 Web Crypto API 的标准算法。要在浏览器中使用它,开发者必须引入第三方库,这些库通常通过 WebAssembly(Wasm)编译以获得接近原生的性能。因此,架构师面临一个艰难的权衡:
选择 PBKDF2:获得原生支持、普遍兼容性和简化的实现,但接受一个在密码学上正在老化、对特定硬件攻击防御较弱的算法。
选择 Argon2:获得当前最顶级的密码哈希安全性,但代价是增加了一个重要的外部依赖(Wasm 模块),提高了应用的复杂性,并且脱离了浏览器原生、经过严格审查的加密实现。
这个困境深刻地揭示了 Web 标准化与密码学研究快速发展之间的张力。对于需要最高安全级别的应用(例如,密码管理器或加密通信工具),引入 Argon2 可能是必要的。而对于许多其他应用,原生、标准的 PBKDF2 提供的安全级别可能仍然是足够且更务实的选择。
4. 机密性与完整性合一:使用 AES-GCM 进行认证加密
在通过 PBKDF2 从用户密码派生出高熵密钥后,下一步就是使用这个密钥来保护实际数据。现代密码学应用不仅要求数据的机密性(confidentiality),即防止未经授权的读取,还要求数据的完整性(integrity)和真实性(authenticity),即确保数据在传输或存储过程中未被篡改。AES-GCM(高级加密标准 - 伽罗瓦/计数器模式)作为一种认证加密与关联数据(AEAD)模式,完美地满足了这些需求。
NIST SP 800-38D 规范剖析
美国国家标准与技术研究院(NIST)在其特别出版物 SP 800-38D 中详细规定了 GCM 模式 7。AES-GCM 的设计巧妙地结合了两种不同的密码学技术:
机密性:通过将 AES 块加密器在计数器(Counter, CTR)模式下运行来实现。CTR 模式将块加密器转换为一个流加密器。它生成一个与明文长度相同的伪随机密钥流,然后将该密钥流与明文进行异或(XOR)运算得到密文。这种方式效率高,可以并行处理,并且加密和解密使用完全相同的操作,简化了实现。
真实性/完整性:通过一个名为 GHASH 的通用哈希函数来实现。GHASH 在一个特殊的数学结构——伽罗瓦域(Galois Field)GF(2128) 上进行运算,对密文和“关联数据”计算出一个认证标签(Authentication Tag)。这个标签就像是数据的加密指纹。
AEAD (Authenticated Encryption with Associated Data):这是 AES-GCM 的一个核心特性。它不仅加密和认证了明文数据,还能对一些额外的、保持明文形式的“关联数据”(AAD)进行认证。在 Web 应用中,AAD 非常有用,可以用来保护那些不需要加密但必须保证其完整性的元数据,例如消息头、版本号、用户 ID 等。如果 AAD 在传输中被篡改,解密过程将会失败。
Nonce(初始化向量 - IV)的至关重要性
在所有关于 AES-GCM 的讨论中,有一个安全要求被反复强调,其重要性高于一切:对于一个给定的密钥,Nonce(也称为 IV)绝不能重复使用。NIST 7 和 IETF 32 的规范都明确指出了这一点。
Nonce 重用会导致灾难性的安全失效。如果两个不同的明文使用相同的密钥和相同的 Nonce 进行加密,攻击者可以通过对两个密文进行异或运算来消除密钥流,从而得到两个明文的异或结果。这会泄露大量关于明文的信息。更严重的是,Nonce 重用允许攻击者通过代数运算恢复出认证密钥(GHASH 的子密钥 H),一旦认证密钥泄露,攻击者就可以伪造任何消息的认证标签,从而完全攻破 AES-GCM 的完整性保护。
因此,正确地生成和管理 Nonce 是实现 AES-GCM 安全的生命线。NIST 推荐使用 96 位(12 字节)的 Nonce,这为随机生成提供了足够大的空间以避免碰撞。在 Web Crypto API 中,应始终使用 crypto.getRandomValues() 来生成每个加密操作所需的 Nonce。
认证标签(Authentication Tag)
认证标签是 AES-GCM 的另一个输出。在解密时,接收方会使用相同的密钥、Nonce、密文和 AAD 重新计算一遍 GHASH,并将结果与接收到的认证标签进行比较。如果两者完全一致,则证明数据是真实的(来自持有密钥的发送方)且完整的(在传输中未被修改)。如果不匹配,subtle.decrypt() 方法将返回一个被拒绝的 Promise,应用程序绝不能使用或信任解密失败的数据。
在 Web Crypto API 中的实现
Web Crypto API 通过 subtle.encrypt() 和 subtle.decrypt() 方法支持 AES-GCM。调用这些方法时,需要提供一个 AesGcmParams 类型的算法参数对象,该对象必须包含以下字段 15:
name: 必须是 "AES-GCM"。
iv: 一个 Uint8Array,包含为此次操作生成的唯一 Nonce。
additionalData (可选): 一个 Uint8Array,包含任何需要认证但不需要加密的关联数据。
tagLength (可选): 认证标签的长度(以位为单位),通常默认为 128。
AES-GCM 被选为 Web Crypto API 的主要 AEAD 密码,是出于对其卓越性能和效率的考量,这在对延迟和计算资源敏感的浏览器环境中至关重要。CTR 模式的加密和 GHASH 认证过程在很大程度上可以并行化,并且能够充分利用现代 CPU 中的专用硬件指令集(如 AES-NI 和 CLMUL),从而实现极高的吞吐量 8。W3C 规范将 AES-GCM 列为推荐算法,确保了其在各大浏览器中的广泛和优化实现 2。
然而,这种性能优势的背后,是将一个严苛且不容出错的责任完全交给了开发者:必须确保 Nonce 的唯一性。这个责任如果处理不当,会悄无声息地摧毁所有的安全保障。NIST 和学术界反复警告 Nonce 重用的危险性 8。在复杂的、异步的 Web 应用程序环境中,要保证 Nonce 的绝对唯一性是一个不小的软件工程挑战。例如,在支持离线功能、多标签页同步或存在并发操作的应用中,一个简单的递增计数器可能会因为状态同步问题而失败。完全依赖随机生成,则要求底层的随机数生成器必须是高质量的,并且在足够大的空间内(如 96 位)才能将碰撞概率降至可接受的水平。
因此,Web Crypto API 提供的强大功能伴随着一个隐藏的、高风险的责任。这解释了为什么 MDN 等文档中充满了对误用 API 的强烈警告 16。在整个复杂的加密技术栈中,Nonce 管理是那个最容易出错、一旦出错后果也最严重的地方。它堪称此架构的“阿喀琉斯之踵”。架构师和开发者必须投入足够的精力来设计和审查 Nonce 生成策略,以确保其在所有可能的使用场景下都能保持唯一性。
5. 架构综合:构建稳健的 CSE 流程
综合前述章节对 Web Crypto API、PBKDF2 和 AES-GCM 的分析,我们可以构建一个完整、稳健的客户端加密与解密流程。本节将通过详细的步骤分解和数据结构建议,将理论转化为可操作的架构蓝图。
端到端加密流程演练
以下是用户数据从明文到加密存储的完整生命周期,并附有对 Web Crypto API 调用的说明 1。
用户输入密码:流程始于用户在客户端界面输入其密码。这是整个安全系统的“根信任”。
生成盐值 (Salt):为本次加密(通常是用户首次注册或设置密码时)生成一个加密学安全的随机盐值。
实现: const salt = window.crypto.getRandomValues(new Uint8Array(16)); (16 字节/128 位是推荐的长度)。
导入密码为密钥材料:将用户输入的密码字符串(通常先通过 TextEncoder 转换为 Uint8Array)导入为一个 CryptoKey 对象,作为密钥派生的基础。
实现: const keyMaterial = await window.crypto.subtle.importKey("raw", passwordBuffer, "PBKDF2", false, ["deriveKey"]);
派生主加密密钥:使用 PBKDF2 算法,结合上一步的密钥材料和生成的盐值,派生出用于 AES-GCM 加密的主密钥。
实现: const key = await window.crypto.subtle.deriveKey({ name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
生成初始化向量 (IV/Nonce):为本次 AES-GCM 加密操作生成一个唯一的、加密学安全的随机 Nonce。
实现: const iv = window.crypto.getRandomValues(new Uint8Array(12)); (12 字节/96 位是 AES-GCM 的推荐长度)。
执行加密:使用派生出的 AES-GCM 密钥和生成的 IV,对明文数据进行加密。
实现: const ciphertext = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, plaintextBuffer);
数据存储策略
加密完成后,必须将多个加密参数与密文本身一同存储,以便未来能够成功解密。一个常见且高效的策略是将所有必要的二进制数据打包成一个单一的数据块(blob),然后进行编码以便于存储 1。
一个推荐的结构是按顺序拼接:盐值 | IV | 密文。
盐值 (Salt):必须存储,因为在解密时需要用它和用户密码重新派生出完全相同的密钥。
IV (Nonce):必须存储,因为它是 AES-GCM 解密过程的必要输入。
密文 (Ciphertext):这是 subtle.encrypt 的输出,它已经包含了认证标签(tag)。
将这三部分拼接后,通常会使用 Base64 编码将其转换为一个字符串。这个字符串可以安全地存储在服务器的任何文本字段中,例如关系型数据库的 TEXT 列或 NoSQL 数据库中的一个 JSON 字段。
解密流程
当用户需要访问其加密数据时,解密流程与加密流程相对应:
数据检索:从服务器获取之前存储的 Base64 编码的加密数据字符串。
解码与解析:对字符串进行 Base64 解码,得到二进制数据块。然后根据预定义的长度(例如,前 16 字节是盐,接下来 12 字节是 IV),将盐、IV 和密文分离开。
重新派生密钥:提示用户输入其密码。使用与加密时完全相同的参数(相同的盐、迭代次数和哈希函数),重复步骤 3 和 4 的密钥派生过程,以生成完全相同的 AES-GCM CryptoKey。
解密与验证:调用 subtle.decrypt() 方法,传入派生出的密钥、解析出的 IV 和密文。
实现: const plaintextBuffer = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, key, ciphertext);
Web Crypto API 的实现在内部会自动使用密文中包含的认证标签进行完整性验证。如果 Promise 成功解析(resolved),则说明密码正确且数据未被篡改,返回的 plaintextBuffer 即为原始明文。如果 Promise 被拒绝(rejected),则表示密码错误或数据已被篡改。应用程序必须妥善处理此错误,绝不能使用解密失败的数据。
CSE 数据结构建议表
为了给开发者提供一个清晰、规范的指导,下表定义了推荐的 CSE 数据存储格式。采用标准化的格式可以消除歧义,减少实现错误,并提高系统的健壮性。
6. 对标 E2EE 原则:从 CSE 到真正的端到端安全
前述章节构建的客户端加密(CSE)模型,在保护服务器上的静态数据方面表现出色。然而,要全面评估其安全级别,必须将其与现代端到端加密(End-to-End Encryption, E2EE)通信系统的黄金标准进行对标。这不仅能揭示其优势,更能明确其局限性,从而帮助架构师根据应用场景做出恰当的技术选型。
核心 E2EE 属性评估
首先,我们根据信息安全的三个基本支柱来评估基线 CSE 模型:
机密性 (Confidentiality):非常强大。由于加密密钥由用户密码派生且仅存在于客户端,服务器永远无法看到数据的明文内容 36。
完整性 (Integrity) 与 真实性 (Authenticity):同样强大。AES-GCM 的认证加密特性确保了任何对存储在服务器上的密文的篡改都会在解密时被检测到,从而防止应用程序处理被篡改的无效数据 37。
“每条消息唯一密钥”原则的审视
用户查询中提到的“每条消息唯一密钥”是现代 E2EE 的核心思想,它直接关系到两个高级安全属性:前向保密性(Forward Secrecy)和后妥协安全性(Post-Compromise Security)。
然而,我们构建的基线 CSE 模型并不满足这一原则。在该模型中,所有数据都由一个从用户密码派生出的单一主密钥进行加密。如果这个主密钥因为某种原因(例如,客户端的恶意软件、用户密码泄露、或未来量子计算破解了 PBKDF2)而被泄露,那么攻击者将能够解密该用户所有过去、现在和未来的数据。这是一个巨大的单点故障。
对比黄金标准:Signal 协议
为了理解真正的“每条消息唯一密钥”如何实现,我们需要考察目前公认的 E2EE 通信协议的黄金标准——Signal 协议 38。Signal 协议的核心是其创新的双棘轮算法(Double Ratchet Algorithm)42。
双棘轮算法巧妙地结合了两种机制来为每一条消息生成一个唯一的、一次性的加密密钥:
对称密钥棘轮 (Symmetric-key Ratchet):这类似于一个哈希链。每发送或接收一条消息,都会使用一个密钥派生函数(KDF)从当前的“链密钥”派生出一个“消息密钥”和一个新的“链密钥”。消息密钥用于加密当前消息,然后被立即销毁。新的链密钥则用于下一次迭代。这确保了即使攻击者获得了某一个消息密钥,也无法计算出之前或之后的消息密钥,从而实现了前向保密性。
Diffie-Hellman 棘轮 (DH Ratchet):为了防止攻击者窃取整个链密钥,双棘轮算法周期性地引入新的熵。当通信的一方用尽了预先生成的密钥时,他们会生成一个新的 DH 密钥对,并将公钥附在消息中发送给对方。对方收到后,会进行一次 DH 密钥交换,并将交换得到的新共享秘密混合到棘轮中,生成一个全新的根密钥和链密钥。这个过程提供了后妥协安全性:即使攻击者在某一时刻完全攻破了设备并窃取了所有密钥状态,一旦双方完成一次新的 DH 交换,棘轮就会“自愈”,后续的消息将重新变得安全。
弥合差距:增强型 CSE 架构
通过借鉴 Signal 协议的思想,我们可以在基线 CSE 模型的基础上构建一个更安全的增强型架构。这个架构的核心思想是,不直接使用 PBKDF2 派生出的密钥作为加密密钥,而是将其作为一个长期存在的根密钥(Root Key)。然后,为每一条需要加密的数据(或每一次会话)生成一个唯一的 Nonce,并使用一个密钥派生函数(如 HKDF,Web Crypto API 也支持)从根密钥和这个 Nonce 派生出一个一次性的数据加密密钥(Data Encryption Key, DEK)。
DEK = HKDF(rootKey, nonce, info_string)
然后使用这个 DEK 和一个新的 IV 来通过 AES-GCM 加密数据。存储时,需要将用于派生 DEK 的 Nonce 与 IV 和密文一同存储。这种“密钥分离”的架构,虽然没有实现双棘轮的全部安全属性,但已经向“每条消息唯一密钥”迈出了一大步,并提供了一定程度的前向保密性(如果 DEK 在使用后能被安全地销毁)。
客户端加密(CSE)与端到端加密(E2EE)通信之间存在着一个至关重要的架构性区别。我们所讨论的基线 WebCrypto+PBKDF2+AES-GCM 模型,是“静态数据客户端加密”的一个优秀实现,其主要目标是保护存储在不可信服务器上的数据。然而,它本身并不提供现代 E2EE 通信系统所期望的高级安全属性,如前向保密性和后妥协安全性。
这个区别对于架构师在进行安全模型选择时至关重要。基线 CSE 模型中,所有数据的安全性都系于一个由密码派生出的主密钥。这个密钥的泄露将导致所有数据的灾难性解密。相比之下,Signal 协议的双棘轮算法 42 专门设计用来应对密钥泄露的场景。通过不断地向前滚动密钥,它保证了即使当前密钥被攻破,过去的消息仍然是安全的(前向保密性)。通过周期性地混入新的 DH 密钥交换结果,它能够在设备被攻破后恢复安全状态,保护未来的消息(后妥协安全性)。
这清晰地表明,用户查询中提到的两个核心原则——“端侧加密”和“每条消息唯一密钥”——并非由同一个系统自动提供。基线 CSE 模型实现了前者,但未能实现后者。因此,架构师的选择必须与应用的具体威胁模型相匹配。例如,对于一个安全的个人笔记应用,其主要威胁是服务器数据泄露,那么基线 CSE 模型可能已经足够。但对于一个安全的即时通讯应用,其威胁模型还必须包括设备被盗或被临时控制的场景,那么就必须在 Web Crypto 的基础原语之上,实现一个更复杂的密钥管理协议,如棘轮算法,才能达到用户所期望的安全级别。
安全属性对比表
为了直观地展示不同架构之间的安全权衡,下表对几种模型的安全属性进行了比较。这有助于架构师根据其应用的安全需求做出明智的决策。
7. 行业采纳与案例研究:现实世界中的 CSE
客户端加密不仅仅是一个理论上的架构模型,它已经被全球领先的科技公司广泛采纳并部署到其核心产品中。通过分析这些现实世界的案例,我们可以洞察 CSE 的最佳实践、演进方向以及在企业环境中的具体应用模式。
谷歌 Workspace 客户端加密分析
谷歌在其 Workspace 套件(包括 Drive、Docs、Gmail 等)中提供了明确的客户端加密(CSE)功能 10。其实现方式极具代表性,展示了企业级 CSE 的成熟形态。谷歌 CSE 的核心架构特点是将密钥管理完全外部化:
外部密钥管理服务 (KMS):用户的加密密钥并非由密码在本地派生,而是由一个客户控制的、外部的 KMS 进行管理。谷歌与多个第三方 KMS 提供商合作,企业也可以自行构建符合其规范的 KMS。
外部身份提供商 (IdP):在访问 KMS 获取密钥之前,用户必须通过企业自己控制的 IdP(如 Okta 或 Azure AD)进行身份验证。
这个模型创建了一个强大的信任边界。谷歌作为云服务提供商,负责存储加密后的数据,但由于它无法访问 IdP 和 KMS,因此在技术上无法解密用户数据。这是一种典型的“零知识”或“零信任”实现,满足了对数据主权和隐私有最高要求的企业客户 10。
微软 Azure 客户端加密分析
微软在其 Azure 云服务中,特别是在 Azure Blob 存储等服务中,也提供了强大的客户端加密支持 9。微软的架构采用了业界广泛使用的“信封加密”(Envelope Encryption)模式:
内容加密密钥 (CEK):客户端 SDK 首先生成一个一次性的、高强度的对称密钥(CEK),用它来直接加密用户数据。
密钥加密密钥 (KEK):然后,客户端使用一个由客户管理的、长期存在的密钥(KEK)来加密这个 CEK。
存储:加密后的数据和加密后的 CEK 一同上传到 Azure 存储。
在这个模型中,KEK 是安全的核心。客户可以完全控制 KEK,例如将其存储在 Azure Key Vault 中,并对其访问策略进行精细化管理,甚至可以使用自带密钥(BYOK)的功能。微软作为平台方,可以访问加密后的 CEK,但由于没有 KEK,同样无法解密用户数据。这种模式在保护数据安全的同时,也提供了灵活高效的密钥管理能力。
IBM Cloudant 客户端加密分析
IBM 在其 Cloudant NoSQL 数据库服务的文档中,也明确建议对存储在数据库中的敏感数据使用客户端加密 11。与谷歌和微软提供的集成化服务不同,IBM 的方式更侧重于为开发者提供指导和最佳实践,鼓励他们在自己的客户端应用程序中实现加密逻辑,然后再将加密后的 JSON 文档发送到 Cloudant。这为开发者提供了最大的灵活性,但同时也要求他们承担更多的实现和安全管理的责任。
更广泛的生态系统:以 Bitwarden 为例
CSE 的应用并不仅限于大型云提供商。在注重安全的开源社区中,它同样是最佳实践。开源密码管理器 Bitwarden 就是一个绝佳的例子。在其安全白皮书中,Bitwarden 明确指出,用户的所有密码库数据在离开本地设备之前,都会使用用户的主密码通过 PBKDF2 派生出的密钥进行 AES-256 加密 45。Bitwarden 的服务器只存储加密后的数据,从未接触过用户的明文密码或主密钥。这展示了本报告所分析的 WebCrypto+PBKDF2+AES-GCM 架构模式在真实产品中的直接应用。
通过对这些行业案例的分析,我们可以发现一个清晰的趋势:在企业环境中,客户端加密正朝着与集中式的、客户控制的密钥管理系统(KMS)和身份提供商(IdP)深度融合的方向发展。这催生了“自带密钥”(BYOK)甚至“持有你自己的密钥”(HYOK)等服务模式,这些模式在满足企业对数据控制、审计和合规性要求的同时,又能充分利用公有云的弹性、可扩展性和成本效益。
一个仅依赖用户密码派生的简单 CSE 模型,在企业应用场景中存在明显不足。例如,它无法支持密钥轮换、无法为合规审计提供访问通道、也无法在用户忘记密码时提供安全的恢复机制。大型企业需要对加密密钥拥有集中的控制权,以满足其复杂的安全策略和法规要求。
为了响应这一需求,谷歌 10 和微软 9 等云巨头纷纷推出了创新的 CSE 框架。这些框架巧妙地将数据存储(在云提供商的服务器上)与密钥管理(由客户通过 KMS/IdP 控制)分离开来。这种混合架构为企业提供了两全其美的解决方案:云提供商负责处理大规模、高持久性存储的复杂工程问题,而客户则通过牢牢掌握密钥来保留对数据访问的最终控制权。
因此,企业级 CSE 的真正趋势,并不仅仅是加密操作本身,而是客户端加密与外部化的、客户控制的信任锚点(KMS/IdP)的集成。这是一个比单纯的基于密码的加密模型更为复杂、也更为强大的架构范式,它正在重新定义云时代的企业数据安全边界。
主要云提供商 CSE 产品对比
8. 安全性分析与战略建议
尽管基于 Web Crypto 的 CSE 架构提供了强大的安全保障,但它并非银弹。其安全性高度依赖于正确的实现和对潜在威胁的全面理解。本节将对该架构进行系统的威胁建模,并为架构师和开发者提供具体的战略建议。
CSE 架构的威胁建模
一个全面的安全分析必须考虑所有可能的攻击向量:
受损的客户端端点:这是最直接的威胁。如果用户的设备感染了恶意软件、安装了恶意的浏览器扩展,或者被物理接触,那么存储在内存中或本地的密钥就有可能被窃取。尽管 CryptoKey 的不透明性提供了一定保护,但高级的恶意软件仍可能通过内存抓取等手段获取密钥材料。
跨站脚本攻击 (XSS):这是 Web 应用中最常见的漏洞之一。一个持久性或反射性的 XSS 漏洞,可能允许攻击者注入恶意脚本。这个脚本可以在用户输入密码时将其捕获,或者利用用户会话中已存在的 CryptoKey 对象在后台静默地解密数据并将其发送到攻击者的服务器。
恶意的服务器端代码:如前文所述,这是 CSE 模型的核心风险。由于执行加密的 JavaScript 代码由服务器提供,一个被攻破的服务器可以分发一个被篡改过的恶意版本。这个版本可能包含后门,用于窃取密码或密钥 20。
侧信道攻击 (Side-Channel Attacks):这是一类更高级的攻击,攻击者不直接攻击加密算法本身,而是通过观察其物理实现(如执行时间、功耗、缓存访问模式)来推断密钥信息。虽然在浏览器环境中实施这类攻击非常困难,但理论上仍然是可能的。
弱密码:整个安全链的强度取决于其最薄弱的一环。如果用户选择了一个简单、易于猜测的密码,那么即使 PBKDF2 设置了极高的迭代次数,也无法抵御针对这个弱密码的字典攻击。
常见的实现陷阱
开发者在实现 CSE 时,很容易陷入以下误区,从而导致严重的安全漏洞:
AES-GCM 中的 Nonce 重用:这是最致命的错误,会直接导致认证密钥泄露和机密性完全失效。
PBKDF2 迭代次数不足:为了追求性能而选择一个过低的迭代次数,会使得暴力破解在经济上变得可行。
不安全的盐/IV 生成:使用非加密级的随机数生成器(如 Math.random())或使用静态的、可预测的盐/IV。
不必要地将密钥设为可导出:违反了最小权限原则,为恶意脚本窃取密钥打开了方便之门。
不当的错误处理:通过加密操作返回的错误信息向攻击者泄露了有用信息。例如,应该让“密码错误”和“数据被篡改”返回相同的通用错误信息,以防止攻击者通过错误类型来验证密码的正确性。
对架构师的建议
何时选择 CSE:当应用需要处理高度敏感的数据,并且出于隐私、安全或法规遵从性的原因,必须在密码学上阻止服务器端访问这些数据时,应优先考虑 CSE 架构。典型的应用场景包括:个人日记、医疗记录、密码管理器、私密通信等。
参数选择:
PBKDF2:在 2024 年,迭代次数应至少从 100,000 次起步,并根据目标设备的性能进行基准测试,以在可接受的延迟(例如,登录时 100-500 毫秒)和最大安全性之间取得平衡。应计划随着计算能力的增长而定期增加迭代次数。
AES-GCM:始终使用 12 字节(96 位)的随机 Nonce,并通过 crypto.getRandomValues 生成。认证标签长度应使用默认的 128 位,除非有非常特殊的性能限制。
密钥生命周期管理:设计一个完整的密钥生命周期策略,包括密钥的派生、使用、存储(加密数据的存储)、以及在必要时的撤销或轮换机制(例如,在用户更改密码时)。
对开发者的建议
API 最佳实践:
始终使用 crypto.getRandomValues 生成盐和 IV。
尽可能将 CryptoKey 对象的 extractable 属性设置为 false。
使用 IndexedDB 存储 CryptoKey 对象,而不是 LocalStorage。IndexedDB 可以直接存储 CryptoKey 对象,而无需将其导出为可序列化的格式,从而更好地保护密钥 18。
防御性编码:
实施严格的内容安全策略(CSP),以限制可以执行脚本的来源,并防止内联脚本和 eval()。
对所有从第三方加载的脚本,使用子资源完整性(SRI)来验证其哈希值。
对所有用户输入进行严格的清理和编码,以从根本上杜绝 XSS 漏洞。
密码策略:
强制执行最小密码长度(NIST SP 800-63B 建议至少 8 个字符,但鼓励更长)27。
在用户设置密码时,通过 API 查询(例如 Have I Been Pwned)检查其是否出现在已知的密码泄露数据库中,并禁止使用已泄露的密码。
9. 结论:Web 客户端信任的未来
本报告的深入分析证实,由 Web Crypto API 驱动,并结合 PBKDF2 和 AES-GCM 的客户端加密模型,确实是当前 Web 应用安全领域一个强大且主流的趋势。它为在不可信服务器上保护静态数据提供了一个标准化的、高效且稳健的解决方案。这一架构不仅在技术上是可行的,而且已经被行业领导者和安全社区广泛采纳,作为应对日益增长的数据隐私和安全挑战的关键策略。报告验证了用户最初的论点,同时为其增添了关键的细微差别和上下文,特别是明确了其在更广泛的端到端加密生态系统中的定位和局限性。
未来展望
展望未来,Web 客户端的信任模型将继续演进,几个关键的技术趋势将塑造其下一代形态:
WebAssembly (Wasm):WebAssembly 正在成为将下一代加密算法高效引入浏览器的关键技术。对于像 Argon2 这样的内存困难型哈希函数,或者新兴的后量子密码(Post-Quantum Cryptography, PQC)算法,通过 Wasm 实现可以获得接近原生的性能。这使得 Web 应用能够采用比 Web Crypto API 标准化进程更快、更先进的密码学技术,从而弥补了当前标准中 PBKDF2 等算法逐渐老化的不足。
新兴标准:对于需要复杂的多方安全通信的场景,如群组聊天,从头构建安全的密钥管理协议(如双棘轮的扩展)是一项艰巨的任务。像消息层安全(Messaging Layer Security, MLS)这样的新兴 IETF 标准,旨在为群组端到端加密提供一个可互操作的、开箱即用的协议框架。未来,这些标准可能会被整合到 Web 平台中,为开发者提供更高级、更易于使用的 E2EE 构建模块。
WebAuthn 与无密码未来:基于密码的认证和密钥派生模型正面临着向无密码认证的根本性转变。Web Authentication API (WebAuthn) 的普及,使用户能够通过硬件安全密钥(如 YubiKey)或平台认证器(如 Windows Hello, Face ID)进行防钓鱼的强认证 46。在 CSE 的背景下,这意味着密钥派生模型将发生革命性变化。未来的架构将不再依赖 PBKDF2 从低熵密码派生密钥,而是可能采用一种“密钥封装”(key wrapping)模型:一个高熵的主密钥在设备上生成并存储,然后使用 WebAuthn 认证器生成的密钥对其进行加密(封装)。用户登录时,通过 WebAuthn 认证成功后,即可解封(unwrap)主密钥。这种模式彻底摆脱了弱密码的风险,将信任锚点牢固地建立在硬件安全之上,代表了客户端信任模型的下一个演进方向。
综上所述,WebCrypto+PBKDF2+AES-GCM 技术栈是当前实现客户端加密的一个坚实基础。然而,它只是一个开始。未来的 Web 安全架构将在其之上,通过 WebAssembly 引入更强的算法,通过 MLS 等新标准简化复杂通信场景,并通过 WebAuthn 最终取代密码,构建一个更加安全、更加值得信赖的去中心化 Web 生态系统。
Works cited
bradyjoslin/webcrypto-example: Demonstrates a way to encrypt and decrypt data using the Web Crypto API - GitHub, accessed on October 10, 2025,
Web Cryptography API - W3C, accessed on October 10, 2025,
Web Cryptography API - Wikipedia, accessed on October 10, 2025,
W3C's suggestion for a Web Cryptography API - Cryptomathic, accessed on October 10, 2025,
PBKDF2 - Wikipedia, accessed on October 10, 2025,
PBKDF2: Password Based Key Derivation - SSLTrust, accessed on October 10, 2025,
NIST SP 800-38D, Recommendationfor Block Cipher Modes of ..., accessed on October 10, 2025,
Current Modes - Block Cipher Techniques | CSRC - National Institute of Standards and Technology, accessed on October 10, 2025,
Azure encryption overview | Microsoft Learn, accessed on October 10, 2025,
About client-side encryption - Google Workspace Admin Help, accessed on October 10, 2025,
Cloudant Security - IBM Cloud Docs, accessed on October 10, 2025,
Web Cryptography API - W3C, accessed on October 10, 2025,
Web Cryptography Level 2 - W3C, accessed on October 10, 2025,
Crypto - Web APIs | MDN - Mozilla, accessed on October 10, 2025,
SubtleCrypto - Web APIs | MDN, accessed on October 10, 2025,
Web Crypto API, accessed on October 10, 2025,
Web Crypto API - Web APIs - MDN Web Docs, accessed on October 10, 2025,
When using client-side encryption in a web application, where should keys be stored?, accessed on October 10, 2025,
SubtleCrypto: digest() method - Web APIs - MDN, accessed on October 10, 2025,
How to best use JavaScript to encrypt client side so the server never sees it?, accessed on October 10, 2025,
PBKDF2 - ASecuritySite.com, accessed on October 10, 2025,
RFC 6070 - PKCS #5: Password-Based Key Derivation Function 2 (PBKDF2) Test Vectors, accessed on October 10, 2025,
RFC 2898: Password-Based Cryptography Specification ... - IETF, accessed on October 10, 2025,
PBKDF2 (Password-Based Key Derivation Function) is a key stretching algorithm. It can be used to hash passwords in a computationally intensive manner, so that dictionary and brute-force attacks are less effective. See CrackStation's Hashing Security Article for instructions on implementing salted password hashing. See https://defuse.ca/php-pbkdf2 - GitHub Gist, accessed on October 10, 2025,
SubtleCrypto: deriveKey() method - Web APIs | MDN - Mozilla, accessed on October 10, 2025,
Password Hashing and Storage - MojoAuth, accessed on October 10, 2025,
NIST Password Guidelines: 2025 Updates & Best Practices - StrongDM, accessed on October 10, 2025,
NIST Special Publication 800-63B, accessed on October 10, 2025,
Publication Moved: NIST SP 800-38D, Recommendation for Block Cipher Modes of Operation - CSRC, accessed on October 10, 2025,
SP 800-38D Rev. 1, Pre-Draft Call for Comments: GCM and GMAC Block Cipher Modes of Operation - NIST CSRC - National Institute of Standards and Technology, accessed on October 10, 2025,
Galois Extended Mode - NIST CSRC, accessed on October 10, 2025,
RFC 5084 - Using AES-CCM and AES-GCM Authenticated Encryption in the Cryptographic Message Syntax (CMS) - IETF Datatracker, accessed on October 10, 2025,
SubtleCrypto: encrypt() method - Web APIs | MDN - Mozilla, accessed on October 10, 2025,
Web Crypto API example - GitHub Pages, accessed on October 10, 2025,
Where to store salt for PBKDF2 and initialization vector for AES via WebCrypto, accessed on October 10, 2025,
End-to-End Encryption: A Technical Perspective - Thomas Bandt, accessed on October 10, 2025,
Study and Analysis of End-to-End Encryption Message Security Using Diffie-Hellman Key Exchange Encryption, accessed on October 10, 2025,
(PDF) Review of End-to-End Encryption for Social Media - ResearchGate, accessed on October 10, 2025,
Signal Protocol - Wikipedia, accessed on October 10, 2025,
CS 528 Project – Signal Secure Messaging Protocol - CS@Purdue, accessed on October 10, 2025,
A Formal Security Analysis of the Signal Messaging Protocol - The Swiss Bay, accessed on October 10, 2025,
Signal >> Documentation, accessed on October 10, 2025,
Cloudant | IBM Cloud API Docs, accessed on October 10, 2025,
Securing your data in IBM Cloudant - IBM Cloud Docs, accessed on October 10, 2025,
Bitwarden Security Whitepaper, accessed on October 10, 2025,
Web Authentication API - MDN - Mozilla, accessed on October 10, 2025,