在Chrome扩展Manifest V3中嵌入libsodium.js的问题
问题背景
我正尝试使用libsodium.js——一个经Emscripten转译为WASM的C语言库,但遇到了Chrome扩展Manifest V3环境下的适配问题。找到的两篇相关参考内容都无法解决问题:
- 旧帖子提到Chrome扩展禁止使用
eval(),而libsodium.js初始化会调用该方法,Manifest V3中只能通过沙盒页面规避,但配套示例并非针对V3版本; - 另一帖子的答案链接已失效,其思路是通过iframe与DOM通信,但我需要在后台服务工作者中使用该库,这个方案不适用。
已尝试的方法及问题
- 沙盒页面加载:在沙盒页面中成功运行了独立构建版
sodium.js,但沙盒页面无法调用chrome.*API,无法与后台服务工作者通信; - 后台服务工作者导入尝试:
- 使用
importScripts()导入:触发CSP违规错误Uncaught (in promise) RuntimeError: Aborted(CompileError: WebAssembly.instantiate(): Refused to compile or instantiate WebAssembly module because neither 'wasm-eval' nor 'unsafe-eval' is an allowed source of script in the following Content Security Policy directive: "script-src 'self'"). Build with -sASSERTIONS for more info.
- 将
manifest.json中background设为module类型后,使用import {_sodium} from '../thirdparty/js/libsodium-wrappers.js'导入模块构建版:Uncaught SyntaxError: The requested module '../thirdparty/js/libsodium-wrappers.js' does not provide an export named '_sodium'
- 直接使用
import '../thirdparty/js/libsodium-wrappers.js'导入:Uncaught TypeError: Cannot read properties of undefined (reading 'sodium')
- 使用
可行解决方案
方案1:调整CSP并使用ES模块版本
- 修改
manifest.json的CSP配置,添加'wasm-eval'权限:{ "manifest_version": 3, "background": { "service_worker": "background.js", "type": "module" }, "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-eval'; object-src 'self'" } } - 下载libsodium.js的ES模块构建版本(需与
libsodium-wrappers.js配套),将文件放在扩展目录的thirdparty/js下; - 在后台服务工作者中正确导入并初始化:
import * as sodium from '../thirdparty/js/libsodium-wrappers.js'; // 初始化libsodium async function initSodium() { await sodium.ready; // 之后即可调用sodium的API,例如: const key = sodium.crypto_secretbox_keygen(); console.log('生成密钥:', key); } initSodium();
方案2:沙盒页面+消息通信
如果无法调整CSP,可通过沙盒页面作为libsodium的运行容器,配合消息机制与后台服务工作者通信:
- 在
manifest.json中配置沙盒页面:{ "manifest_version": 3, "sandbox": { "pages": ["sandbox.html"] }, "background": { "service_worker": "background.js" } } - 创建
sandbox.html,加载独立构建版sodium.js并处理消息:<script src="thirdparty/js/sodium.js"></script> <script> // 等待libsodium初始化完成 sodium.ready.then(() => { // 监听来自后台的消息 window.addEventListener('message', async (event) => { if (event.source !== parent) return; const { type, data } = event.data; let result; // 根据消息类型执行对应操作 switch(type) { case 'crypto_secretbox_keygen': result = sodium.crypto_secretbox_keygen(); break; // 可添加更多操作类型 } // 向后台返回结果 parent.postMessage({ type, result }, '*'); }); }); </script> - 在后台服务工作者中创建沙盒iframe并通信:
async function getSandboxPort() { const iframe = document.createElement('iframe'); iframe.src = chrome.runtime.getURL('sandbox.html'); iframe.style.display = 'none'; document.body.appendChild(iframe); return new Promise(resolve => { window.addEventListener('message', (event) => { if (event.source === iframe.contentWindow) { // 建立双向通信通道 const port = { send: (type, data) => iframe.contentWindow.postMessage({ type, data }, '*'), onMessage: (callback) => { window.addEventListener('message', (event) => { if (event.source === iframe.contentWindow) { callback(event.data); } }); } }; resolve(port); } }, { once: true }); }); } // 使用示例 async function generateKey() { const port = await getSandboxPort(); return new Promise(resolve => { port.onMessage((message) => { if (message.type === 'crypto_secretbox_keygen') { resolve(message.result); } }); port.send('crypto_secretbox_keygen'); }); }
内容的提问来源于stack exchange,提问作者Jacob Jewett

