Skip to content

Commit 668d62f

Browse files
committed
Add Ghidra decompilation download page
1 parent 284e200 commit 668d62f

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed

src/components/navbar.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const currentPagePath = getPathnameWithoutExtension(Astro.url);
4040
<li><a class="dropdown-item" class:list={{ active: currentPagePath === '/simulator' }} href="/simulator">District Simulator</a></li>
4141
<li><a class="dropdown-item" class:list={{ active: currentPagePath === '/cards' }} href="/cards">Venture Cards</a></li>
4242
<li><a class="dropdown-item" class:list={{ active: currentPagePath === '/editor' }} href="/editor">Board Yaml Editor</a></li>
43+
<li><a class="dropdown-item" class:list={{ active: currentPagePath === '/ghidra' }} href="/ghidra">Ghidra Decompilation</a></li>
4344
</ul>
4445
</li>
4546
</ul>

src/lib/otp.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
2+
function repeatKey(key: Uint8Array, contentLength: number): Uint8Array {
3+
const repeatedKey = new Uint8Array(contentLength);
4+
for (let i = 0; i < contentLength; i++) {
5+
repeatedKey[i] = key[i % key.length];
6+
}
7+
return repeatedKey;
8+
}
9+
10+
export function concatenateKey(originalKey: Uint8Array, additionalKey: Uint8Array): Uint8Array {
11+
const extendedKey = new Uint8Array(originalKey.length + additionalKey.length);
12+
extendedKey.set(originalKey, 0);
13+
extendedKey.set(additionalKey, originalKey.length);
14+
return extendedKey;
15+
}
16+
17+
export function encryptWithKey(content: ArrayBuffer, key: Uint8Array): Uint8Array {
18+
const extendedKey = repeatKey(key, content.byteLength);
19+
const contentBytes = new Uint8Array(content);
20+
const encryptedBytes = new Uint8Array(contentBytes.length);
21+
22+
for (let i = 0; i < contentBytes.length; i++) {
23+
encryptedBytes[i] = contentBytes[i] ^ extendedKey[i];
24+
}
25+
26+
return encryptedBytes;
27+
}
28+
29+
export function decryptWithKey(encryptedContent: ArrayBuffer, key: Uint8Array): ArrayBuffer {
30+
const extendedKey = repeatKey(key, encryptedContent.byteLength);
31+
const encryptedBytes = new Uint8Array(encryptedContent);
32+
const decryptedBytes = new Uint8Array(encryptedBytes.length);
33+
34+
for (let i = 0; i < encryptedBytes.length; i++) {
35+
decryptedBytes[i] = encryptedBytes[i] ^ extendedKey[i];
36+
}
37+
38+
return decryptedBytes.buffer;
39+
}
40+
41+
export function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
42+
return new Promise((resolve, reject) => {
43+
const reader = new FileReader();
44+
reader.onload = () => resolve(reader.result as ArrayBuffer);
45+
reader.onerror = reject;
46+
reader.readAsArrayBuffer(file);
47+
});
48+
}
49+
50+
export async function computeSHA256(arrayBuffer: ArrayBuffer): Promise<ArrayBuffer> {
51+
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
52+
return hashBuffer;
53+
}
54+
55+
export function arrayBufferToHex(buffer: ArrayBuffer): string {
56+
const bytes = new Uint8Array(buffer);
57+
const hexArray = Array.from(bytes).map(byte => byte.toString(16).padStart(2, '0'));
58+
return hexArray.join('');
59+
}
60+
61+
export async function download(url: string): Promise<ArrayBuffer> {
62+
// Fetch the file from the server
63+
const response = await fetch(url);
64+
65+
// Check if the request was successful
66+
if (!response.ok) {
67+
throw new Error(`Network response was not ok: ${response.statusText}`);
68+
}
69+
70+
return await response.arrayBuffer();
71+
}

src/pages/ghidra.astro

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
---
2+
import Layout from '~/layouts/layout.astro';
3+
const isDev = import.meta.env.DEV;
4+
---
5+
6+
<Layout title="Home" heading="Ghidra Decompilation">
7+
<div class="container mt-4">
8+
<div class="mb-4">
9+
This page allows you to download the current efforts on decompiling the Boom Street <mark>main.dol</mark> file using the Ghidra decompilation project. You
10+
need the original <mark>sys/main.dol</mark> and <mark>files/main.sel</mark> files in order to generate the Ghidra Zip File.
11+
</div>
12+
13+
<div class="mb-4 requirements">
14+
<h2 class="section-title">Requirements</h2>
15+
<ul>
16+
<li>
17+
Following files from original vanilla extracted Boom Street (ST7P01) game folder:
18+
<ul>
19+
<li>sys/main.dol</li>
20+
<li>files/main.sel</li>
21+
</ul>
22+
</li>
23+
<li>Ghidra 11.1.2 or newer</li>
24+
<li><a href="https://github.com/Cuyler36/Ghidra-GameCube-Loader" target="_blank">Ghidra GameCube Loader</a> extension</li>
25+
</ul>
26+
</div>
27+
28+
<div class="mb-4">
29+
<h2 class="section-title">Download Ghidra Zip File</h2>
30+
<form id="downloadForm">
31+
<div class="mb-3">
32+
<label for="mainDol" class="form-label">Input sys/main.dol file:</label>
33+
<input
34+
class="form-control"
35+
type="file"
36+
id="mainDol"
37+
accept=".dol"
38+
data-checksum="91951bb24deb9dc56bb8feba328a633cbf03271d4f1bf0b58bc4b5160482f1db"
39+
required
40+
/>
41+
<div class="invalid-feedback">Please select a valid `sys/main.dol` file.</div>
42+
</div>
43+
<div class="mb-3">
44+
<label for="mainSel" class="form-label">Input files/main.sel file:</label>
45+
<input
46+
class="form-control"
47+
type="file"
48+
id="mainSel"
49+
accept=".sel"
50+
data-checksum="625a43ec3c59494ae5d37391083e65951dbfa6815c9f361ffe554416de2f08e7"
51+
required
52+
/>
53+
<div class="invalid-feedback">Please select a valid `files/main.sel` file.</div>
54+
</div>
55+
<button type="submit" id="btnDownload" class="btn btn-primary">
56+
<span class="spinner-border spinner-border-sm" id="loading" style="display: none;" role="status" aria-hidden="true"></span>
57+
Download main.gzf
58+
</button>
59+
</form>
60+
<div class="mt-4">
61+
<p>Once downloaded, you can import the file into Ghidra.</p>
62+
</div>
63+
</div>
64+
65+
<div class="mb-4" style={isDev ? '' : 'display: none;'}>
66+
<h2 class="section-title">Create Ghidra Zip One-Time Pad File (.gzf.otp)</h2>
67+
<p>This section is for development purposes only.</p>
68+
<form id="createForm">
69+
<div class="mb-3">
70+
<label for="mainDolCreate" class="form-label">Input sys/main.dol file:</label>
71+
<input
72+
class="form-control"
73+
type="file"
74+
id="mainDolCreate"
75+
accept=".dol"
76+
data-checksum="91951bb24deb9dc56bb8feba328a633cbf03271d4f1bf0b58bc4b5160482f1db"
77+
required
78+
/>
79+
<div class="invalid-feedback">Please select a valid `sys/main.dol` file.</div>
80+
</div>
81+
<div class="mb-3">
82+
<label for="mainSelCreate" class="form-label">Input files/main.sel file:</label>
83+
<input
84+
class="form-control"
85+
type="file"
86+
id="mainSelCreate"
87+
accept=".sel"
88+
data-checksum="625a43ec3c59494ae5d37391083e65951dbfa6815c9f361ffe554416de2f08e7"
89+
required
90+
/>
91+
<div class="invalid-feedback">Please select a valid `files/main.sel` file.</div>
92+
</div>
93+
<div class="mb-3" style={isDev ? '' : 'display: none;'}>
94+
<label for="mainGzfCreate" class="form-label">Input main.gzf file:</label>
95+
<input class="form-control" type="file" id="mainGzfCreate" accept=".gzf" required />
96+
</div>
97+
<button type="submit" id="btnCreate" class="btn btn-primary">
98+
<span class="spinner-border spinner-border-sm" id="loadingCreate" style="display: none;" role="status" aria-hidden="true"></span>
99+
Create main.gzf.otp
100+
</button>
101+
</form>
102+
<div class="mt-4">
103+
<p>Once created, the generated main.gzf.otp file can then be uploaded to <a href="https://github.com/FortuneStreetModding/ghidra-boom-street">GitHub</a></p>
104+
</div>
105+
</div>
106+
</div>
107+
</Layout>
108+
109+
<script>
110+
import { decryptWithKey } from '~/lib/otp';
111+
import { readFileAsArrayBuffer, computeSHA256, arrayBufferToHex, concatenateKey, encryptWithKey, download } from '~/lib/otp';
112+
113+
// Download .gzf
114+
const mainDolInput = document.getElementById('mainDol') as HTMLInputElement;
115+
const mainSelInput = document.getElementById('mainSel') as HTMLInputElement;
116+
const buttonDownloadInput = document.getElementById('btnDownload') as HTMLButtonElement;
117+
const loadingInput = document.getElementById('loading') as HTMLSpanElement;
118+
// Create .gzf.otp
119+
const mainDolCreateInput = document.getElementById('mainDolCreate') as HTMLInputElement;
120+
const mainSelCreateInput = document.getElementById('mainSelCreate') as HTMLInputElement;
121+
const mainGzfCreateInput = document.getElementById('mainGzfCreate') as HTMLInputElement;
122+
const loadingCreateInput = document.getElementById('loadingCreate') as HTMLSpanElement;
123+
const buttonCreateInput = document.getElementById('btnCreate') as HTMLButtonElement;
124+
125+
mainDolInput.addEventListener('change', function () {
126+
validateFileInput(this as HTMLInputElement);
127+
});
128+
129+
mainSelInput.addEventListener('change', function () {
130+
validateFileInput(this as HTMLInputElement);
131+
});
132+
133+
mainDolCreateInput.addEventListener('change', function () {
134+
validateFileInput(this as HTMLInputElement);
135+
});
136+
137+
mainSelCreateInput.addEventListener('change', function () {
138+
validateFileInput(this as HTMLInputElement);
139+
});
140+
141+
async function validateFileInput(input: HTMLInputElement) {
142+
const file = input.files![0];
143+
const fileType = input.accept.split(',').map(type => type.replace('.', ''));
144+
let isValid = file && fileType.includes(file.name.split('.').pop()!);
145+
146+
if (isValid) {
147+
const expectedChecksum = input.dataset.checksum!;
148+
const actualChecksum = arrayBufferToHex(await computeSHA256(await readFileAsArrayBuffer(file)));
149+
isValid = expectedChecksum === actualChecksum;
150+
}
151+
152+
if (isValid) {
153+
input.classList.remove('is-invalid');
154+
input.classList.add('is-valid');
155+
} else {
156+
input.classList.remove('is-valid');
157+
input.classList.add('is-invalid');
158+
}
159+
return isValid;
160+
}
161+
162+
document.getElementById('downloadForm')!.addEventListener('submit', async function (event) {
163+
event.preventDefault();
164+
loadingInput.style.display = 'inline-block';
165+
buttonDownloadInput.disabled = true;
166+
try {
167+
168+
if (!(await validateFileInput(mainDolInput)) && (await validateFileInput(mainSelInput))) {
169+
return;
170+
}
171+
172+
const mainDolFile = mainDolInput.files![0];
173+
const mainSelFile = mainSelInput.files![0];
174+
const dolContent = await readFileAsArrayBuffer(mainDolFile);
175+
const selContent = await readFileAsArrayBuffer(mainSelFile);
176+
const gzfOtpContent = await download("/ghidra-boom-street/main.gzf.otp");
177+
178+
const key = concatenateKey(new Uint8Array(dolContent), new Uint8Array(selContent));
179+
const gzfContent = decryptWithKey(gzfOtpContent, key);
180+
181+
// Convert the content string to a Blob object
182+
const blob = new Blob([gzfContent], { type: 'application/octet-stream' });
183+
184+
// Create an <a> element and set its attributes
185+
const a = document.createElement('a');
186+
a.style.display = 'none';
187+
a.href = URL.createObjectURL(blob);
188+
a.download = `boom-street.gzf`;
189+
190+
// Append the <a> element to the DOM and click on it to trigger the download
191+
document.body.appendChild(a);
192+
a.click();
193+
194+
// Remove the <a> element from the DOM
195+
document.body.removeChild(a);
196+
197+
// Clean up the Blob object
198+
setTimeout(() => URL.revokeObjectURL(a.href), 1500);
199+
200+
} finally {
201+
loadingInput.style.display = 'none';
202+
buttonDownloadInput.disabled = false;
203+
}
204+
});
205+
206+
document.getElementById('createForm')!.addEventListener('submit', async function (event) {
207+
event.preventDefault();
208+
loadingCreateInput.style.display = 'inline-block';
209+
buttonCreateInput.disabled = true;
210+
try {
211+
212+
if (!(await validateFileInput(mainDolCreateInput)) && (await validateFileInput(mainSelCreateInput))) {
213+
return;
214+
}
215+
216+
const mainDolFile = mainDolCreateInput.files![0];
217+
const mainSelFile = mainSelCreateInput.files![0];
218+
const mainGzfFile = mainGzfCreateInput.files![0];
219+
const dolContent = await readFileAsArrayBuffer(mainDolFile);
220+
const selContent = await readFileAsArrayBuffer(mainSelFile);
221+
const gzfContent = await readFileAsArrayBuffer(mainGzfFile);
222+
223+
const key = concatenateKey(new Uint8Array(dolContent), new Uint8Array(selContent));
224+
const gzfOtpContent = encryptWithKey(gzfContent, key);
225+
226+
// Convert the content to a Blob object
227+
const blob = new Blob([gzfOtpContent], { type: 'application/octet-stream' });
228+
229+
// Create an <a> element and set its attributes
230+
const a = document.createElement('a');
231+
a.style.display = 'none';
232+
a.href = URL.createObjectURL(blob);
233+
a.download = `main.gzf.otp`;
234+
235+
// Append the <a> element to the DOM and click on it to trigger the download
236+
document.body.appendChild(a);
237+
a.click();
238+
239+
// Remove the <a> element from the DOM
240+
document.body.removeChild(a);
241+
242+
// Clean up the Blob object
243+
setTimeout(() => URL.revokeObjectURL(a.href), 1500);
244+
245+
} finally {
246+
loadingCreateInput.style.display = 'none';
247+
buttonCreateInput.disabled = false;
248+
}
249+
});
250+
</script>

0 commit comments

Comments
 (0)