Skip to content

Commit

Permalink
Add Ghidra decompilation download page
Browse files Browse the repository at this point in the history
  • Loading branch information
Deflaktor committed Aug 25, 2024
1 parent 284e200 commit 668d62f
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/components/navbar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const currentPagePath = getPathnameWithoutExtension(Astro.url);
<li><a class="dropdown-item" class:list={{ active: currentPagePath === '/simulator' }} href="/simulator">District Simulator</a></li>
<li><a class="dropdown-item" class:list={{ active: currentPagePath === '/cards' }} href="/cards">Venture Cards</a></li>
<li><a class="dropdown-item" class:list={{ active: currentPagePath === '/editor' }} href="/editor">Board Yaml Editor</a></li>
<li><a class="dropdown-item" class:list={{ active: currentPagePath === '/ghidra' }} href="/ghidra">Ghidra Decompilation</a></li>
</ul>
</li>
</ul>
Expand Down
71 changes: 71 additions & 0 deletions src/lib/otp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

function repeatKey(key: Uint8Array, contentLength: number): Uint8Array {
const repeatedKey = new Uint8Array(contentLength);
for (let i = 0; i < contentLength; i++) {
repeatedKey[i] = key[i % key.length];
}
return repeatedKey;
}

export function concatenateKey(originalKey: Uint8Array, additionalKey: Uint8Array): Uint8Array {
const extendedKey = new Uint8Array(originalKey.length + additionalKey.length);
extendedKey.set(originalKey, 0);
extendedKey.set(additionalKey, originalKey.length);
return extendedKey;
}

export function encryptWithKey(content: ArrayBuffer, key: Uint8Array): Uint8Array {
const extendedKey = repeatKey(key, content.byteLength);
const contentBytes = new Uint8Array(content);
const encryptedBytes = new Uint8Array(contentBytes.length);

for (let i = 0; i < contentBytes.length; i++) {
encryptedBytes[i] = contentBytes[i] ^ extendedKey[i];
}

return encryptedBytes;
}

export function decryptWithKey(encryptedContent: ArrayBuffer, key: Uint8Array): ArrayBuffer {
const extendedKey = repeatKey(key, encryptedContent.byteLength);
const encryptedBytes = new Uint8Array(encryptedContent);
const decryptedBytes = new Uint8Array(encryptedBytes.length);

for (let i = 0; i < encryptedBytes.length; i++) {
decryptedBytes[i] = encryptedBytes[i] ^ extendedKey[i];
}

return decryptedBytes.buffer;
}

export function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as ArrayBuffer);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}

export async function computeSHA256(arrayBuffer: ArrayBuffer): Promise<ArrayBuffer> {
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
return hashBuffer;
}

export function arrayBufferToHex(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
const hexArray = Array.from(bytes).map(byte => byte.toString(16).padStart(2, '0'));
return hexArray.join('');
}

export async function download(url: string): Promise<ArrayBuffer> {
// Fetch the file from the server
const response = await fetch(url);

// Check if the request was successful
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}

return await response.arrayBuffer();
}
250 changes: 250 additions & 0 deletions src/pages/ghidra.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
---
import Layout from '~/layouts/layout.astro';
const isDev = import.meta.env.DEV;
---

<Layout title="Home" heading="Ghidra Decompilation">
<div class="container mt-4">
<div class="mb-4">
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
need the original <mark>sys/main.dol</mark> and <mark>files/main.sel</mark> files in order to generate the Ghidra Zip File.
</div>

<div class="mb-4 requirements">
<h2 class="section-title">Requirements</h2>
<ul>
<li>
Following files from original vanilla extracted Boom Street (ST7P01) game folder:
<ul>
<li>sys/main.dol</li>
<li>files/main.sel</li>
</ul>
</li>
<li>Ghidra 11.1.2 or newer</li>
<li><a href="https://github.com/Cuyler36/Ghidra-GameCube-Loader" target="_blank">Ghidra GameCube Loader</a> extension</li>
</ul>
</div>

<div class="mb-4">
<h2 class="section-title">Download Ghidra Zip File</h2>
<form id="downloadForm">
<div class="mb-3">
<label for="mainDol" class="form-label">Input sys/main.dol file:</label>
<input
class="form-control"
type="file"
id="mainDol"
accept=".dol"
data-checksum="91951bb24deb9dc56bb8feba328a633cbf03271d4f1bf0b58bc4b5160482f1db"
required
/>
<div class="invalid-feedback">Please select a valid `sys/main.dol` file.</div>
</div>
<div class="mb-3">
<label for="mainSel" class="form-label">Input files/main.sel file:</label>
<input
class="form-control"
type="file"
id="mainSel"
accept=".sel"
data-checksum="625a43ec3c59494ae5d37391083e65951dbfa6815c9f361ffe554416de2f08e7"
required
/>
<div class="invalid-feedback">Please select a valid `files/main.sel` file.</div>
</div>
<button type="submit" id="btnDownload" class="btn btn-primary">
<span class="spinner-border spinner-border-sm" id="loading" style="display: none;" role="status" aria-hidden="true"></span>
Download main.gzf
</button>
</form>
<div class="mt-4">
<p>Once downloaded, you can import the file into Ghidra.</p>
</div>
</div>

<div class="mb-4" style={isDev ? '' : 'display: none;'}>
<h2 class="section-title">Create Ghidra Zip One-Time Pad File (.gzf.otp)</h2>
<p>This section is for development purposes only.</p>
<form id="createForm">
<div class="mb-3">
<label for="mainDolCreate" class="form-label">Input sys/main.dol file:</label>
<input
class="form-control"
type="file"
id="mainDolCreate"
accept=".dol"
data-checksum="91951bb24deb9dc56bb8feba328a633cbf03271d4f1bf0b58bc4b5160482f1db"
required
/>
<div class="invalid-feedback">Please select a valid `sys/main.dol` file.</div>
</div>
<div class="mb-3">
<label for="mainSelCreate" class="form-label">Input files/main.sel file:</label>
<input
class="form-control"
type="file"
id="mainSelCreate"
accept=".sel"
data-checksum="625a43ec3c59494ae5d37391083e65951dbfa6815c9f361ffe554416de2f08e7"
required
/>
<div class="invalid-feedback">Please select a valid `files/main.sel` file.</div>
</div>
<div class="mb-3" style={isDev ? '' : 'display: none;'}>
<label for="mainGzfCreate" class="form-label">Input main.gzf file:</label>
<input class="form-control" type="file" id="mainGzfCreate" accept=".gzf" required />
</div>
<button type="submit" id="btnCreate" class="btn btn-primary">
<span class="spinner-border spinner-border-sm" id="loadingCreate" style="display: none;" role="status" aria-hidden="true"></span>
Create main.gzf.otp
</button>
</form>
<div class="mt-4">
<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>
</div>
</div>
</div>
</Layout>

<script>
import { decryptWithKey } from '~/lib/otp';
import { readFileAsArrayBuffer, computeSHA256, arrayBufferToHex, concatenateKey, encryptWithKey, download } from '~/lib/otp';

// Download .gzf
const mainDolInput = document.getElementById('mainDol') as HTMLInputElement;
const mainSelInput = document.getElementById('mainSel') as HTMLInputElement;
const buttonDownloadInput = document.getElementById('btnDownload') as HTMLButtonElement;
const loadingInput = document.getElementById('loading') as HTMLSpanElement;
// Create .gzf.otp
const mainDolCreateInput = document.getElementById('mainDolCreate') as HTMLInputElement;
const mainSelCreateInput = document.getElementById('mainSelCreate') as HTMLInputElement;
const mainGzfCreateInput = document.getElementById('mainGzfCreate') as HTMLInputElement;
const loadingCreateInput = document.getElementById('loadingCreate') as HTMLSpanElement;
const buttonCreateInput = document.getElementById('btnCreate') as HTMLButtonElement;

mainDolInput.addEventListener('change', function () {
validateFileInput(this as HTMLInputElement);
});

mainSelInput.addEventListener('change', function () {
validateFileInput(this as HTMLInputElement);
});

mainDolCreateInput.addEventListener('change', function () {
validateFileInput(this as HTMLInputElement);
});

mainSelCreateInput.addEventListener('change', function () {
validateFileInput(this as HTMLInputElement);
});

async function validateFileInput(input: HTMLInputElement) {
const file = input.files![0];
const fileType = input.accept.split(',').map(type => type.replace('.', ''));
let isValid = file && fileType.includes(file.name.split('.').pop()!);

if (isValid) {
const expectedChecksum = input.dataset.checksum!;
const actualChecksum = arrayBufferToHex(await computeSHA256(await readFileAsArrayBuffer(file)));
isValid = expectedChecksum === actualChecksum;
}

if (isValid) {
input.classList.remove('is-invalid');
input.classList.add('is-valid');
} else {
input.classList.remove('is-valid');
input.classList.add('is-invalid');
}
return isValid;
}

document.getElementById('downloadForm')!.addEventListener('submit', async function (event) {
event.preventDefault();
loadingInput.style.display = 'inline-block';
buttonDownloadInput.disabled = true;
try {

if (!(await validateFileInput(mainDolInput)) && (await validateFileInput(mainSelInput))) {
return;
}

const mainDolFile = mainDolInput.files![0];
const mainSelFile = mainSelInput.files![0];
const dolContent = await readFileAsArrayBuffer(mainDolFile);
const selContent = await readFileAsArrayBuffer(mainSelFile);
const gzfOtpContent = await download("/ghidra-boom-street/main.gzf.otp");

const key = concatenateKey(new Uint8Array(dolContent), new Uint8Array(selContent));
const gzfContent = decryptWithKey(gzfOtpContent, key);

// Convert the content string to a Blob object
const blob = new Blob([gzfContent], { type: 'application/octet-stream' });

// Create an <a> element and set its attributes
const a = document.createElement('a');
a.style.display = 'none';
a.href = URL.createObjectURL(blob);
a.download = `boom-street.gzf`;

// Append the <a> element to the DOM and click on it to trigger the download
document.body.appendChild(a);
a.click();

// Remove the <a> element from the DOM
document.body.removeChild(a);

// Clean up the Blob object
setTimeout(() => URL.revokeObjectURL(a.href), 1500);

} finally {
loadingInput.style.display = 'none';
buttonDownloadInput.disabled = false;
}
});

document.getElementById('createForm')!.addEventListener('submit', async function (event) {
event.preventDefault();
loadingCreateInput.style.display = 'inline-block';
buttonCreateInput.disabled = true;
try {

if (!(await validateFileInput(mainDolCreateInput)) && (await validateFileInput(mainSelCreateInput))) {
return;
}

const mainDolFile = mainDolCreateInput.files![0];
const mainSelFile = mainSelCreateInput.files![0];
const mainGzfFile = mainGzfCreateInput.files![0];
const dolContent = await readFileAsArrayBuffer(mainDolFile);
const selContent = await readFileAsArrayBuffer(mainSelFile);
const gzfContent = await readFileAsArrayBuffer(mainGzfFile);

const key = concatenateKey(new Uint8Array(dolContent), new Uint8Array(selContent));
const gzfOtpContent = encryptWithKey(gzfContent, key);

// Convert the content to a Blob object
const blob = new Blob([gzfOtpContent], { type: 'application/octet-stream' });

// Create an <a> element and set its attributes
const a = document.createElement('a');
a.style.display = 'none';
a.href = URL.createObjectURL(blob);
a.download = `main.gzf.otp`;

// Append the <a> element to the DOM and click on it to trigger the download
document.body.appendChild(a);
a.click();

// Remove the <a> element from the DOM
document.body.removeChild(a);

// Clean up the Blob object
setTimeout(() => URL.revokeObjectURL(a.href), 1500);

} finally {
loadingCreateInput.style.display = 'none';
buttonCreateInput.disabled = false;
}
});
</script>

0 comments on commit 668d62f

Please sign in to comment.