Skip to content

Instantly share code, notes, and snippets.

@Pldare
Last active April 8, 2026 21:58
Show Gist options
  • Select an option

  • Save Pldare/ebf704c752a8d77ff9603d4adfe54083 to your computer and use it in GitHub Desktop.

Select an option

Save Pldare/ebf704c752a8d77ff9603d4adfe54083 to your computer and use it in GitHub Desktop.
vroid web view vrm decrypt
using System;
using System.Security.Cryptography;
using System.IO;
using System.IO.Compression;
namespace Vroid
{
class Vroiddec
{
static void Main(string[] args)
{
using (FileStream fs = new FileStream(args[0],FileMode.Open)) {
using (BinaryReader bs = new BinaryReader(fs)) {
int buff_size=(int)(fs.Length)-48;
RijndaelManaged rDel=new RijndaelManaged();
rDel.IV=bs.ReadBytes(16);
rDel.Key=bs.ReadBytes(32);
rDel.Mode=CipherMode.CBC;
byte[] resultarray=rDel.CreateDecryptor().TransformFinalBlock(bs.ReadBytes(buff_size),0,buff_size);
using(MemoryStream ms =new MemoryStream(resultarray)){
using(GZipStream gzs=new GZipStream(ms,CompressionMode.Decompress)){
using(FileStream df=new FileStream(args[0]+".dec",FileMode.OpenOrCreate,FileAccess.Write))
{
int data;
while((data=gzs.ReadByte())!=-1)
{
df.WriteByte((byte)data);
}
}
}
}
}
}
log_msg("Done!");
}
static void log_msg(string msg)
{
Console.WriteLine(msg);
}
}
}
import requests
import os
import json
import argparse
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/114.0"
HOST = "https://hub.vroid.com"
API_VERSION = "11"
def download_model_from_vroid(model_id, subdir=None):
#model_path_base = os.path.join(
# subdir if subdir else args.directory, model_id)
model_api_url = f"{HOST}/api/character_models/{model_id}"
#print(model_api_url)
return model_api_url
def get_user_model_ids(user_id):
model_ids = []
api_url = f"{HOST}/api/users/{user_id}/character_models?antisocial_or_hate_usage=&characterization_allowed_user=&corporate_commercial_use=&credit=&modification=&personal_commercial_use=&political_or_religious_usage=&redistribution=&sexual_expression=&violent_expression="
page_num = 1
while api_url:
user_r = requests.get(
api_url, headers={"User-Agent": USER_AGENT, "X-Api-Version": API_VERSION})
if not user_r.ok:
print(
f"[user:{user_id}:page:{page_num}] got bad response from vroid hub, {user_r.status_code}")
break
user_j = user_r.json()
if "next" in user_j["_links"]:
api_url = HOST + user_j["_links"]["next"]["href"]
else:
api_url = None
for model in user_j["data"]:
model_ids.append(model["id"])
print(f"[user:{user_id}] found {len(model_ids)} models")
return model_ids
def download_user_from_vroid(user_id):
user_api_url = f"{HOST}/api/users/{user_id}"
user_api_r = requests.get(user_api_url, headers={
"User-Agent": USER_AGENT, "X-Api-Version": API_VERSION})
if not user_api_r.ok:
print(
f"[user:{user_id}:api] got bad response from vroid hub, user might not exist, {user_api_r.status_code}")
return
user_api_j = user_api_r.json()
username = user_api_j["data"]["user"]["name"]
#user_base_path = os.path.join(args.directory, f"{username} ({user_id})")
#if not os.path.isdir(user_base_path):
# os.makedirs(user_base_path)
#json_path = f"{user_base_path}.info.json"
#if args.write_info_json:
# with open(json_path, "w") as json_file:
# json_file.write(json.dumps(user_api_j["data"]))
# print(f"[user:{user_id}:api] wrote '{os.path.basename(json_path)}'")
model_ids = get_user_model_ids(user_id)
all_url=[]
for model_id in model_ids:
url=download_model_from_vroid(model_id)#, user_base_path)
all_url.append(url)
return all_url
if __name__ == "__main__":
#parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
#parser.add_argument("-d", "--directory", type=str,
# help="save directory (defaults to current)", default=os.getcwd())
#args = parser.parse_args()
#print(args.directory)
#https://hub.vroid.com/en/users/36667771
download_user_from_vroid(36667771)
#use project https://github.com/Pldare/vrh-deobfuscator/tree/MatchAndBatch
#use example
#python dlhelp.py users_id or vroid_model_view_url
import os,sys
import re
import checkusersmodel
import time
def dl_glb(url,sdir):
print(url)
os.system("node src/index2.js {}".format(url))
url=sys.argv[1]
if "http" in url:
dl_glb(url,"model")
else:
all_url=checkusersmodel.download_user_from_vroid(url)
cou=len(all_url)
idd=1
for gurl in all_url:
print("{}/{}".format(idd,cou))
dl_glb(gurl,str(url))
#time.sleep(0)
idd+=1
@Pldare
Copy link
Copy Markdown
Author

Pldare commented Feb 13, 2025

I checked the differences between the downloaded VRM and the VRM in the browser, and found that the vertex coordinate data had undergone some transformations

@twnlink
Copy link
Copy Markdown

twnlink commented Mar 2, 2025

VRoid Hub obfuscates the mesh of the preview models. You can read more on this here: https://toon.link/blog/1740863435/borrowing-intellectual-property.

@kotx
Copy link
Copy Markdown

kotx commented Mar 2, 2025

Nice post!

@Zer0TheObserver
Copy link
Copy Markdown

I checked the differences between the downloaded VRM and the VRM in the browser, and found that the vertex coordinate data had undergone some transformations

true dude, I hope this will shows how it works:
https://toon.link/blog/1740863435/borrowing-intellectual-property

@Pldare
Copy link
Copy Markdown
Author

Pldare commented Mar 1, 2026

Hello everyone, you can finally use this project https://github.com/Pldare/vrh-deobfuscator/tree/MatchAndBatch to download all or a single model from your user's collection (this was mostly made for research purposes). If you like vroid model, please support model author.

@5Sleeves
Copy link
Copy Markdown

Fix Summary
Root cause: VRoid Hub updated their deobfuscation logic in two ways:

New timestamp 58245139 was added with seed value 9402684
Seed computation changed — all entries now use XOR (value ^ hashInt) instead of the old mixed approach (XOR for 4058237768, addition with overflow wrapping for others)
How I found it: Downloaded VRoid Hub's frontend JS, extracted webpack module 73648 (the seed map computation module), decoded its RC4-obfuscated string table, and read the actual seed values and computation formula directly from VRoid Hub's production code.

Changes to

src/index.js
:

Added 58245139: 9402684 to seedMapStartingState
Changed

computeSeedMap
to use XOR universally for all seed entries

New timestamp 58245139 was added with seed value 9402684

const seedMapStartingState = {
	98756153: 74670526,
	53816997: 38325553,
	4058237768: 1289559305,
	58245139: 9402684,
};

and a small update for XOR

const computeSeedMap = async (inputValue, url) => {
	console.log("Computing seed map...");
	if (url?.includes("s=op")) {
		const apiVersionOffset = ["/v1/", "/v2/"].some((prefix) =>
			url.includes(prefix),
		)
			? 6
			: 5;
		const path = url.split("/").slice(apiVersionOffset).join("/");

		const hash = createHash("sha1");
		hash.update(new TextEncoder().encode(path));
		const hashBuffer = hash.digest().buffer;

		const hashInt = new DataView(hashBuffer).getInt32(
			hashBuffer.byteLength - 4,
			true,
		);
		return Object.fromEntries(
			Object.entries(seedMapStartingState).map(([key, value]) => [
				key,
				// VRoid Hub now uses XOR for all seed entries
				value ^ hashInt,
			]),
		);
	}

	return Object.fromEntries(
		Object.entries(seedMapStartingState).map(([key, value]) => [
			key,
			value + Number.parseInt(inputValue, 10),
		]),
	);
};

@Sketchfab-Downloader
Copy link
Copy Markdown

Sketchfab-Downloader commented Apr 3, 2026

how to find the value for seed timestamp 2664362260 ?

any method to find values for new timestamps in future ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment