一次对服务器3D模型资源加密的实践

最近领导要求防止线上的3d模型被人下载,查阅了线上关于这块的讨论,基本答案都是无法组件被下载,只能增加被下载的难度,比如这篇文章,https://forum.babylonjs.com/t...。但领导已经说了很简单,不就是二进制数据吗,加密一下就行了,所以只能先想一想解决方案了
实现方案 目前能想到的方案,简单来说就是:
  1. 服务端返回3D模型数据的时候,对返回的内容进行加密
  2. 前端对返回的内容进行解密
由于前端解密有性能要求,所以考虑通过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();

打开效果如图
一次对服务器3D模型资源加密的实践
文章图片

服务端加密 用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); } });

加密之后,模型打不开了,看下返回
一次对服务器3D模型资源加密的实践
文章图片

加密之前是
一次对服务器3D模型资源加密的实践
文章图片

说明加密是有效果的,现在下载的模型应该也是打不开的
前端解密 加入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); } })() ); });

打开控制台看下,是不是注册成功了
一次对服务器3D模型资源加密的实践
文章图片

解密 解密代码可以用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 = Mutex::new(HashMap::new()); }#[wasm_bindgen] pub fn decrypt(mut buffer: &mut[u8], key: &str) -> Vec { 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; }) );

刷新,模型又能正常加载了,查看控制台
一次对服务器3D模型资源加密的实践
文章图片

解密成功
下载保护 但是下载文件的时候,也会经过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文件发现模型打不开了,网页加载是正常的
总结 【一次对服务器3D模型资源加密的实践】本文只处理了glb文件,对于.obj .gltf等资源文件还没进行测试。
此方案目前只是一种尝试,还未在线上进行使用,不知道使用效果如何。如果大佬们有更好的方案,或者对此方案有补充的地方,希望可以在评论区进行补充。
本文源代码地址

    推荐阅读