"""Read-only inventory of the installed game's signed local resource headers."""
import concurrent.futures, hashlib, json, pathlib, re, struct, sys
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
ROOT=pathlib.Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/Win64')
KEY=json.loads(pathlib.Path('artifacts/ship-model-reference/local-asset-public-key.json').read_text())
N,E=int(KEY['n']),KEY['e']
OUTPUT=pathlib.Path(sys.argv[1]); OUTPUT.parent.mkdir(parents=True,exist_ok=True)
PATTERN=re.compile(rb'galax|milky|skybox|skydome|starfield|star_field|nebula|sector|catalog|stellarforge|starcatalog',re.I)
def decode(path, limit=None):
with path.open('rb') as f: data=f.read(limit or -1)
if data[:4] not in (b'FREA',b'FREE'): raise ValueError('Unknown archive')
raw=pow(int.from_bytes(data[6:262],'big'),E,N).to_bytes(256,'big')
sep=raw.index(b'\0',2)
if sep<10: raise ValueError('Invalid signature')
message=raw[sep+1:]; size=struct.unpack_from('<I',message)[0]
key=bytes(x^path.name.lower().encode()[i%40] for i,x in enumerate(message[4:36]))
end=262+(len(data)-262)//16*16
decrypt=Cipher(algorithms.AES(key),modes.ECB()).decryptor()
return (message[36:]+decrypt.update(data[262:end])+decrypt.finalize())[:size]
def scan(directory):
count=0; found=[]
for path in directory.iterdir():
if not path.is_file(): continue
with path.open('rb') as f: data=f.read(262)
if data[:4]!=b'FREA': continue
count+=1
raw=pow(int.from_bytes(data[6:262],'big'),E,N).to_bytes(256,'big')
sep=raw.find(b'\0',2)
if sep<10: continue
header=raw[sep+37:]
if not PATTERN.search(header): continue
prefix=decode(path,262+8192)
strings=[m.group().decode('ascii') for m in re.finditer(rb'[A-Za-z_][A-Za-z0-9_.:/ -]{4,}',prefix)]
found.append({'path':str(path),'bytes':path.stat().st_size,'strings':strings[:28]})
return count,found
if __name__=='__main__':
rows=[]; total=0
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as pool:
for count,found in pool.map(scan,sorted(p for p in ROOT.iterdir() if p.is_dir())):
total+=count; rows+=found
OUTPUT.write_text(json.dumps({'count':total,'resources':rows},indent=2))
print('scanned',total,'candidates',len(rows),flush=True)
"""Read-only inventory of the installed game's signed local resource headers."""
import concurrent.futures, hashlib, json, pathlib, re, struct, sys
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
ROOT=pathlib.Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/Win64')
KEY=json.loads(pathlib.Path('artifacts/ship-model-reference/local-asset-public-key.json').read_text())
N,E=int(KEY['n']),KEY['e']
OUTPUT=pathlib.Path(sys.argv[1]); OUTPUT.parent.mkdir(parents=True,exist_ok=True)
PATTERN=re.compile(rb'galax|milky|skybox|skydome|starfield|star_field|nebula|sector|catalog|stellarforge|starcatalog',re.I)
def decode(path, limit=None):
with path.open('rb') as f: data=f.read(limit or -1)
if data[:4] not in (b'FREA',b'FREE'): raise ValueError('Unknown archive')
raw=pow(int.from_bytes(data[6:262],'big'),E,N).to_bytes(256,'big')
sep=raw.index(b'\0',2)
if sep<10: raise ValueError('Invalid signature')
message=raw[sep+1:]; size=struct.unpack_from('<I',message)[0]
key=bytes(x^path.name.lower().encode()[i%40] for i,x in enumerate(message[4:36]))
end=262+(len(data)-262)//16*16
decrypt=Cipher(algorithms.AES(key),modes.ECB()).decryptor()
return (message[36:]+decrypt.update(data[262:end])+decrypt.finalize())[:size]
def scan(directory):
count=0; found=[]
for path in directory.iterdir():
if not path.is_file(): continue
with path.open('rb') as f: data=f.read(262)
if data[:4]!=b'FREA': continue
count+=1
raw=pow(int.from_bytes(data[6:262],'big'),E,N).to_bytes(256,'big')
sep=raw.find(b'\0',2)
if sep<10: continue
header=raw[sep+37:]
if not PATTERN.search(header): continue
prefix=decode(path,262+8192)
strings=[m.group().decode('ascii') for m in re.finditer(rb'[A-Za-z_][A-Za-z0-9_.:/ -]{4,}',prefix)]
found.append({'path':str(path),'bytes':path.stat().st_size,'strings':strings[:28]})
return count,found
if __name__=='__main__':
rows=[]; total=0
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as pool:
for count,found in pool.map(scan,sorted(p for p in ROOT.iterdir() if p.is_dir())):
total+=count; rows+=found
OUTPUT.write_text(json.dumps({'count':total,'resources':rows},indent=2))
print('scanned',total,'candidates',len(rows),flush=True)