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

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

打开效果如图

image.png

服务端加密

用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);
  }
});

加密之后,模型打不开了,看下返回

image.png

加密之前是

image.png
说明加密是有效果的,现在下载的模型应该也是打不开的

前端解密

加入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);
      }
    })()
  );
});

打开控制台看下,是不是注册成功了

image.png

解密

解密代码可以用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;
  })
);

刷新,模型又能正常加载了,查看控制台

image.png

解密成功

下载保护

但是下载文件的时候,也会经过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等资源文件还没进行测试。
此方案目前只是一种尝试,还未在线上进行使用,不知道使用效果如何。如果大佬们有更好的方案,或者对此方案有补充的地方,希望可以在评论区进行补充。
本文源代码地址

阅读 1.4k

前端开发
前端成长之路

不忘初心

104 声望
4 粉丝
0 条评论

不忘初心

104 声望
4 粉丝
文章目录
宣传栏