共计 4378 个字符,预计需要花费 11 分钟才能阅读完成。
最近领导要求避免线上的 3d 模型被人下载,查阅了线上对于这块的探讨,根本答案都是无奈组件被下载,只能减少被下载的难度,比方这篇文章,https://forum.babylonjs.com/t…。但领导曾经说了很简略,不就是二进制数据吗,加密一下就行了,所以只能先想一想解决方案了
实现计划
目前能想到的计划,简略来说就是:
- 服务端返回 3D 模型数据的时候,对返回的内容进行加密
- 前端对返回的内容进行解密
因为前端解密有性能要求,所以思考通过 WebAssembly+Service Worker 进行解密操作,如果不理解这 2 个技术,自行查阅相干材料
初始化场景
基于 babylon.js,导入一个 glb 格局的模型, 基于 webpack 搭建一个简略的我的项目
import {Scene, Engine, SceneLoader} from "@babylonjs/core";
import "@babylonjs/loaders/glTF";
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const engine = new Engine(canvas);
engine.setSize(window.innerWidth, window.innerHeight);
const scene = new Scene(engine);
scene.createDefaultCameraOrLight(true, true, true);
SceneLoader.AppendAsync("/static/models/", "Xbot.glb").then((scene) => {});
function render() {engine.runRenderLoop(() => {scene.render();
});
}
render();
关上成果如图
服务端加密
用 Node.js 的 crypto 模块,返回数据时对数据进行加密
首先定义一个 key
const key = Buffer.from(
"6b65796b65796b65796b65796b65796b65796b65796b6579",
"hex"
).toString("utf8");
而后对指定的资源进行加密,这里仅以 glb 文件为例
app.get("/*.glb$/", function (req, res) {
// 先固定门路为例
const filepath = path.join(__dirname, "./public/models/Xbot.glb");
console.log("filepath:", filepath);
if (!fs.existsSync(filepath)) {res.status(404).send("");
} else {
const cipher = crypto.createCipheriv(
"aes-192-ctr",
key,
Buffer.alloc(16, 0)
);
const buf = Buffer.from(filepath);
res.setHeader("Content-Type", "application/octet-stream");
fs.createReadStream(buf).pipe(cipher).pipe(res);
// fs.createReadStream(buf).pipe(res);
}
});
加密之后,模型打不开了,看下返回
加密之前是
阐明加密是有成果的,当初下载的模型应该也是打不开的
前端解密
退出 service worker 相干代码
入口文件先注册
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/sw.js")
.then(function (reg) {
// registration worked
console.log("Registration succeeded. Scope is" + reg.scope);
})
.catch(function (error) {
// registration failed
console.log("Registration failed with" + error);
});
}
增加一个 sw.ts 文件,通过 sw 拦挡 fetch 事件,对数据进行解密
self.addEventListener("install", (event) => {console.log("installing");
});
self.addEventListener("activate", (event) => {console.log("activating");
});
const finish = () => {};
const decrypt = (v) => {};
self.addEventListener("fetch", function (event: any) {
event.respondWith((async function () {
const url = event.request.url;
if (event.request.url.endsWith(".glb")) {const response = await fetch(event.request);
if (response.status !== 200) return response;
const reader = response.body.getReader();
const stream = new ReadableStream({start(controller) {function push() {reader.read().then(({done, value}) => {console.log("value:", value);
if (done) {controller.close();
finish(url);
return;
}
controller.enqueue(decrypt(value, url));
push();});
}
push();},
});
return new Response(stream);
} else {return fetch(event.request);
}
})());
});
关上控制台看下,是不是注册胜利了
解密
解密代码能够用 c /c++ 或者 rust 等语言编写,而后编译为 wasm,这里应用 rust 进行解密
lib.rs
extern crate wasm_bindgen;
extern crate aes_ctr;
extern crate hex;
#[macro_use]
extern crate lazy_static;
use aes_ctr::Aes192Ctr;
use aes_ctr::stream_cipher::generic_array::GenericArray;
use aes_ctr::stream_cipher::{NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek};
use std::collections::HashMap;
use std::sync::Mutex;
use wasm_bindgen::prelude::*;
lazy_static! {static ref cipherMap:Mutex<HashMap<String,Mutex<Aes192Ctr>>> = Mutex::new(HashMap::new());
}
#[wasm_bindgen]
pub fn decrypt(mut buffer: &mut[u8], key: &str) -> Vec<u8> {let mut cipherMapLock = cipherMap.lock().unwrap();
let stringKey = String::from(key);
if !cipherMapLock.contains_key(&stringKey)
{let cipherKey = hex::decode("6b65796b65796b65796b65796b65796b65796b65796b6579").unwrap();
cipherMapLock.insert(stringKey.to_string(), Mutex::new(Aes192Ctr::new_var(&cipherKey, &[0; 16]).unwrap()));
}
let mut cipher = cipherMapLock.get(&stringKey).unwrap().lock().unwrap();
cipher.apply_keystream(&mut buffer);
buffer[..].to_vec()}
#[wasm_bindgen]
pub fn finish(key: &str) {cipherMap.lock().unwrap().remove(&String::from(key));
()}
编译为 2 个文件
- lib.js
- lib.wasm
而后引入 sw.ts 文件
importScripts(`/static/wasm/lib.js`);
WebAssembly.compileStreaming(fetch(`/static/wasm/lib.wasm`)).then((mod) =>
WebAssembly.instantiate(mod, { imports: {} }).then((instance) => {self.wasm = instance.exports;})
);
刷新,模型又能失常加载了,查看控制台
解密胜利
下载爱护
然而下载文件的时候,也会通过 sw 进行解密,还是能够下载。剖析 FetchEvent,发现下载的时候 event.request.referrer
是空的,就临时用这个作为下载和加载资源的辨别规范。批改 fetch 代码为
start(controller) {function push() {reader.read().then(({done, value}) => {console.log("value:", value);
if (done) {controller.close();
finish(url);
return;
}
if (event.request.referrer)
controller.enqueue(decrypt(value, url));
else controller.enqueue(value);
push();});
}
push();},
当初在下载 glb 文件发现模型打不开了,网页加载是失常的
总结
本文只解决了 glb 文件,对于.obj .gltf 等资源文件还没进行测试。
此计划目前只是一种尝试,还未在线上进行应用,不晓得应用成果如何。如果大佬们有更好的计划,或者对此计划有补充的中央,心愿能够在评论区进行补充。
本文源代码地址