最近领导要求避免线上的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();

关上成果如图

服务端加密

用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等资源文件还没进行测试。
此计划目前只是一种尝试,还未在线上进行应用,不晓得应用成果如何。如果大佬们有更好的计划,或者对此计划有补充的中央,心愿能够在评论区进行补充。
本文源代码地址