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
@DatTaiwanGuy
Copy link
Copy Markdown

@641i130
Copy link
Copy Markdown

641i130 commented Mar 16, 2022

The GZip looks correct from my testings, It's something we're missing in the file that we're decrypting I think. The file has a texture basis that the program must be looking for. In blender I tried to import it and get this error:
image

@641i130
Copy link
Copy Markdown

641i130 commented Mar 16, 2022

Heres the error I get with a GLB/GLTF validator:
https://clbin.com/7wnf9
(https://github.khronos.org/glTF-Validator/)

@DatTaiwanGuy
Copy link
Copy Markdown

DatTaiwanGuy commented Mar 17, 2022 via email

@DatTaiwanGuy
Copy link
Copy Markdown

DatTaiwanGuy commented Mar 17, 2022 via email

@sr229
Copy link
Copy Markdown

sr229 commented Dec 29, 2022

The GZip looks correct from my testings, It's something we're missing in the file that we're decrypting I think. The file has a texture basis that the program must be looking for. In blender I tried to import it and get this error: image

I think extensions can be ignored or removed from the headers so it can render properly.

@641i130
Copy link
Copy Markdown

641i130 commented Dec 31, 2022

The GZip looks correct from my testings, It's something we're missing in the file that we're decrypting I think. The file has a texture basis that the program must be looking for. In blender I tried to import it and get this error: image

I think extensions can be ignored or removed from the headers so it can render properly.

Any references on doing this? Or tools that do this already?

@sr229
Copy link
Copy Markdown

sr229 commented Jan 3, 2023

The GZip looks correct from my testings, It's something we're missing in the file that we're decrypting I think. The file has a texture basis that the program must be looking for. In blender I tried to import it and get this error: image

I think extensions can be ignored or removed from the headers so it can render properly.

Any references on doing this? Or tools that do this already?

Best way to do is probably read the file as-is then writing out the extension-specific bits to their VRM-standard counterparts and exporting them. This will lead to a semi-broken VRM but can be fixable if it reads properly in Blender. Unfortunately, this is the first time this has been done so we will have to write this ourselves.

@darbdarb
Copy link
Copy Markdown

The GZip looks correct from my testings, It's something we're missing in the file that we're decrypting I think. The file has a texture basis that the program must be looking for. In blender I tried to import it and get this error: image

I think extensions can be ignored or removed from the headers so it can render properly.

Any references on doing this? Or tools that do this already?

Best way to do is probably read the file as-is then writing out the extension-specific bits to their VRM-standard counterparts and exporting them. This will lead to a semi-broken VRM but can be fixable if it reads properly in Blender. Unfortunately, this is the first time this has been done so we will have to write this ourselves.

any update on this? i know its been awhile but i manged to get to this stage, when i try to remove the pixiv texture basis i get a decode error UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf7 in position 128055: invalid start byte

@saltacc
Copy link
Copy Markdown

saltacc commented Sep 10, 2023

i was able to get the model to at least import by modifying the import_gltf2_with_indices method of the blender vrm importer with

        json_dict['extensionsRequired'].pop(0)
        for i in range(len(json_dict['textures'])):
            if 'extensions' in json_dict['textures'][i]:
                json_dict['textures'][i].pop('extensions')

(it's just a dumb hack to remove the extension requirement)

However, the meshes seem to not be quite right
image

I will keep looking into this and update if I find more

@darbdarb
Copy link
Copy Markdown

i was able to get the model to at least import by modifying the import_gltf2_with_indices method of the blender vrm importer with

        json_dict['extensionsRequired'].pop(0)
        for i in range(len(json_dict['textures'])):
            if 'extensions' in json_dict['textures'][i]:
                json_dict['textures'][i].pop('extensions')

(it's just a dumb hack to remove the extension requirement)

However, the meshes seem to not be quite right image

I will keep looking into this and update if I find more

nice nice keep us updated :)

@Pldare
Copy link
Copy Markdown
Author

Pldare commented Oct 28, 2023

hihihi Everyone
I’m glad that everyone pays attention to this code of mine
I share some json parsing methods of gltf models for everyone to facilitate extraction and error correction.
get block info

import json
import sys
with open(sys.argv[1],encoding='utf-8') as f:
    data = json.load(f)


for i in data.keys():
    a=data[i]
    #print(type(a))

accessors = data["accessors"]
buffer_views = data["bufferViews"]
type_num_dict = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}

type_dict={
    5120: "BYTE",
    5121: "UNSIGNED_BYTE",
    5122: "SHORT",
    5123: "UNSIGNED_SHORT",
    5124: "INT",
    5125: "UNSIGNED_INT",
    5126: "FLOAT"
}

for accessor_index, accessor in enumerate(accessors):
    type_count = type_num_dict[accessor["type"]]
    #ct=accessor["componentType"]
    pos=buffer_views[accessor["bufferView"]]["byteOffset"]
    count=accessor["count"]
    ac=accessor["componentType"]
    print(accessor_index,count,type_dict[ac],type_count==1,pos)

get mesh info

print("mesh")
for n, mesh in enumerate(data["meshes"]):
    #print(n,mesh.keys())
    for j,primive in enumerate(mesh["primitives"]):
        #print(primive)
        if primive["mode"] != 4:
            raise "1"
        dbb=primive["indices"]
        print(dbb)
        
        va=primive["attributes"]
        print(va)

The id in meshinfo corresponds to the id in blockinfo
I will provide the complete model extraction code later

@darbdarb
Copy link
Copy Markdown

hihihi Everyone I’m glad that everyone pays attention to this code of mine I share some json parsing methods of gltf models for everyone to facilitate extraction and error correction. get block info

import json
import sys
with open(sys.argv[1],encoding='utf-8') as f:
    data = json.load(f)


for i in data.keys():
    a=data[i]
    #print(type(a))

accessors = data["accessors"]
buffer_views = data["bufferViews"]
type_num_dict = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}

type_dict={
    5120: "BYTE",
    5121: "UNSIGNED_BYTE",
    5122: "SHORT",
    5123: "UNSIGNED_SHORT",
    5124: "INT",
    5125: "UNSIGNED_INT",
    5126: "FLOAT"
}

for accessor_index, accessor in enumerate(accessors):
    type_count = type_num_dict[accessor["type"]]
    #ct=accessor["componentType"]
    pos=buffer_views[accessor["bufferView"]]["byteOffset"]
    count=accessor["count"]
    ac=accessor["componentType"]
    print(accessor_index,count,type_dict[ac],type_count==1,pos)

get mesh info

print("mesh")
for n, mesh in enumerate(data["meshes"]):
    #print(n,mesh.keys())
    for j,primive in enumerate(mesh["primitives"]):
        #print(primive)
        if primive["mode"] != 4:
            raise "1"
        dbb=primive["indices"]
        print(dbb)
        
        va=primive["attributes"]
        print(va)

The id in meshinfo corresponds to the id in blockinfo I will provide the complete model extraction code later

ur amazing <3, after 2 years i didnt expect u to comment but here u are and im here for it, keep up the good work ive got notifcations on for this post

@DerpyMario
Copy link
Copy Markdown

any updates?

@CetaceanNation
Copy link
Copy Markdown

VRoid is now using ZSTD in place of GZIP. I wrote a Python script which handles the new decompression along with the decryption https://github.com/CetaceanNation/misc-scripts/blob/main/vroid-hub-downloader/vroid-hub-downloader.py#L52. If @Pldare completes their extraction code it would be nice to incorporate or at least add to my workflow.

@OwariSync
Copy link
Copy Markdown

i was able to get the model to at least import by modifying the import_gltf2_with_indices method of the blender vrm importer with

        json_dict['extensionsRequired'].pop(0)
        for i in range(len(json_dict['textures'])):
            if 'extensions' in json_dict['textures'][i]:
                json_dict['textures'][i].pop('extensions')

(it's just a dumb hack to remove the extension requirement)

However, the meshes seem to not be quite right image

I will keep looking into this and update if I find more

Sorry for what might be an obvious question, but how can I implement this as well? Using Cetacean's new script, I was able to get a model, but I can't import it due to this extension requirement.

@seed93
Copy link
Copy Markdown

seed93 commented Jan 19, 2024

How to crack this extension?
Even skip this extension, the geometry is wrong as before. Any ideas?

@TenjoinWai
Copy link
Copy Markdown

Tried opening this on this model but the resulting VRM seems to have blanked out textures, is there something I'm missing or I'm using the incorrect endpoint?

I modernized the script a bit and documented a lot of things so everyone can understand how it works now (I used .NET 6.0 here).

using System.Security.Cryptography;
using System.IO.Compression;

namespace Vroid
{
    class VroidDecode
     {
         public static void Main(string[] args) 
         {

            // read the file from arguments, then get the binary representation
            var fs = new FileStream(args[0], FileMode.Open);
            var bs = new BinaryReader(fs);

            // VRoid Hub Encrypted VRMs usually have a the following data structure:
            // * The first 16 bytes is the Initialization Vector
            // * 32 bytes is the actual AES key
            // * The cipher is AES-CBC
            // * Then the rest of the VRM is Gzipped compressed for web transfer.
            // 
            // with this in mind, we now try to access those specific bits first.
            int bufferSize = ((int)fs.Length)-48;
            using Aes aes = Aes.Create();
            aes.IV = bs.ReadBytes(16);
            aes.Key = bs.ReadBytes(32);
            aes.Mode = CipherMode.CBC;

            // now everything is set, we now try to decrypt it!
            var decrypted = aes.CreateDecryptor().TransformFinalBlock(bs.ReadBytes(bufferSize), 0, bufferSize);

            // of course now we got the decrypted part, we still have to deal with the Gzip compression.
            var decryptedStream = new MemoryStream(decrypted);
            var decompressedStream = new GZipStream(decryptedStream, CompressionMode.Decompress);
            var finalStream = new FileStream(args[0] + ".dec", FileMode.OpenOrCreate, FileAccess.Write);

            int data;
            while ((data = decompressedStream.ReadByte()) != -1)
            {
                finalStream.WriteByte((byte)data);
            }

            Console.WriteLine($"Done! Saved as {args[0]}.dec");

        }
     }
}

I have downloaded a model from vroidhub in Feb 2021. all i got is a xs3 file. the model itself may be vrm or pmx format since it could display vmd motion online. and when i process the xs2 file with the cs code, things always go wrong. I wonder how to decypt this xs3 file correctly, could you give some suggestions? the files are here:https://github.com/TenjoinWai/symphony-miku/tree/main

@Mika-Ra
Copy link
Copy Markdown

Mika-Ra commented Mar 31, 2024

Just think this will be helpful?
https://inside.pixiv.blog/2024/02/05/120000

@TenjoinWai
Copy link
Copy Markdown

Just think this will be helpful? https://inside.pixiv.blog/2024/02/05/120000

now i get a decrypted .bin file. it seems that this .bin file contains a pmx model and its textures and meshes. but i cannnot read this file. hope somebody could give me some suggestions. https://github.com/TenjoinWai/symphony-miku

@jdanny2002
Copy link
Copy Markdown

Just think this will be helpful? https://inside.pixiv.blog/2024/02/05/120000

now i get a decrypted .bin file. it seems that this .bin file contains a pmx model and its textures and meshes. but i cannnot read this file. hope somebody could give me some suggestions. https://github.com/TenjoinWai/symphony-miku

Have you ever found a way to use these models?

@jdanny2002
Copy link
Copy Markdown

VRoid is now using ZSTD in place of GZIP. I wrote a Python script which handles the new decompression along with the decryption https://github.com/CetaceanNation/misc-scripts/blob/main/vroid-hub-downloader/vroid-hub-downloader.py#L52. If @Pldare completes their extraction code it would be nice to incorporate or at least add to my workflow.

Hi excuse me to reply to you too, but i wanted to know if there's a way to import the glb models downloaded with your script to Blender, or any program at all... With blender i keep getting that PIXIV texture error

@pcerdo123alv
Copy link
Copy Markdown

pcerdo123alv commented Feb 6, 2025

hihihi Everyone I’m glad that everyone pays attention to this code of mine I share some json parsing methods of gltf models for everyone to facilitate extraction and error correction. get block info

import json
import sys
with open(sys.argv[1],encoding='utf-8') as f:
    data = json.load(f)


for i in data.keys():
    a=data[i]
    #print(type(a))

accessors = data["accessors"]
buffer_views = data["bufferViews"]
type_num_dict = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}

type_dict={
    5120: "BYTE",
    5121: "UNSIGNED_BYTE",
    5122: "SHORT",
    5123: "UNSIGNED_SHORT",
    5124: "INT",
    5125: "UNSIGNED_INT",
    5126: "FLOAT"
}

for accessor_index, accessor in enumerate(accessors):
    type_count = type_num_dict[accessor["type"]]
    #ct=accessor["componentType"]
    pos=buffer_views[accessor["bufferView"]]["byteOffset"]
    count=accessor["count"]
    ac=accessor["componentType"]
    print(accessor_index,count,type_dict[ac],type_count==1,pos)

get mesh info

print("mesh")
for n, mesh in enumerate(data["meshes"]):
    #print(n,mesh.keys())
    for j,primive in enumerate(mesh["primitives"]):
        #print(primive)
        if primive["mode"] != 4:
            raise "1"
        dbb=primive["indices"]
        print(dbb)
        
        va=primive["attributes"]
        print(va)

The id in meshinfo corresponds to the id in blockinfo I will provide the complete model extraction code later

Hi, is there any update about the complete model extraction code?

@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