Skip to content

Commit d75a869

Browse files
committed
Clean up firmware flasher a bit
1 parent 4c751c8 commit d75a869

4 files changed

Lines changed: 112 additions & 89 deletions

File tree

src/lib/EspTool/FlashManager.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,10 @@ export default class FlashManager {
316316
}
317317
}
318318

319-
async flash(data: ArrayBuffer, eraseAll: boolean, onProgress: (progress: number) => void) {
319+
async flash(data: Uint8Array, eraseAll: boolean, onProgress: (progress: number) => void) {
320320
if (!this.loader) return false;
321321

322-
function arrayBufferToString(buffer: ArrayBuffer) {
323-
const array = new Uint8Array(buffer);
322+
function arrayBufferToString(array: Uint8Array) {
324323
let str = '';
325324
for (let i = 0; i < array.length; ++i) {
326325
str += String.fromCharCode(array[i]);

src/lib/api/firmwareCDN.ts

Lines changed: 100 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,125 @@
1-
import { toast } from 'svelte-sonner';
1+
import { HashBuffer } from '$lib/utils/crypto';
2+
3+
export const FirmwareChannels = ['stable', 'beta', 'develop'] as const;
4+
export type FirmwareChannel = (typeof FirmwareChannels)[number];
5+
6+
const BASE_URL = 'https://firmware.openshock.org';
7+
8+
const versionUrl = (channel: string) => `${BASE_URL}/version-${channel}.txt`;
9+
const boardsListUrl = (version: string) => `${BASE_URL}/${version}/boards.txt`;
10+
const boardFileUrl = (version: string, board: string, fileName: string) =>
11+
`${BASE_URL}/${version}/${board}/${fileName}`;
212

313
async function DownloadText(url: string) {
4-
try {
5-
const response = await fetch(url);
6-
if (!response.ok) return null;
7-
const text = await response.text();
8-
return text.trim();
9-
} catch (e) {
10-
console.error(e);
11-
toast.error(`Failed to fetch ${url}`);
12-
return null;
13-
}
14+
const response = await fetch(url);
15+
if (!response.ok)
16+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
17+
const text = await response.text();
18+
return text.trim();
1419
}
1520
async function DownloadLines(url: string) {
1621
const text = await DownloadText(url);
17-
if (!text) return null;
18-
1922
return text.split('\n').map((x) => x.trim());
2023
}
21-
async function DownloadBinary(url: string, onProgress: (progress: number) => void) {
24+
async function DownloadBinary(url: string) {
2225
const response = await fetch(url);
23-
if (!response.ok) return null;
24-
25-
const contentLength = parseInt(response.headers.get('content-length')?.trim() ?? '0');
26-
const reader = response.body?.getReader();
27-
if (!reader) return null;
28-
29-
console.log(`Downloading ${url} (${contentLength} bytes)`);
30-
31-
let receivedLength = 0;
32-
const chunks: Uint8Array[] = [];
33-
for (;;) {
34-
const { done, value } = await reader.read();
35-
if (done) break;
36-
if (!value) continue;
37-
chunks.push(value);
38-
if (contentLength > 0) {
39-
receivedLength += value.length;
40-
onProgress(receivedLength / contentLength);
41-
}
42-
}
43-
44-
const blob = new Blob(chunks);
45-
46-
return await blob.arrayBuffer();
26+
if (!response.ok)
27+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
28+
return await response.bytes();
4729
}
4830

49-
export const FirmwareChannels = ['stable', 'beta', 'develop'] as const;
50-
export type FirmwareChannel = (typeof FirmwareChannels)[number];
51-
52-
export async function FetchChannelVersion(channel: FirmwareChannel) {
53-
return (await DownloadText(`https://firmware.openshock.org/version-${channel}.txt`))?.trim();
31+
export function FetchChannelVersion(channel: FirmwareChannel) {
32+
return DownloadText(versionUrl(channel));
5433
}
5534

5635
export function FetchVersionBoards(version: string) {
57-
return DownloadLines(`https://firmware.openshock.org/${version}/boards.txt`);
36+
return DownloadLines(boardsListUrl(version));
37+
}
38+
39+
export function DownloadBoardBinary(version: string, board: string, filename: string) {
40+
return DownloadBinary(boardFileUrl(version, board, filename));
5841
}
5942

60-
export function DownloadFirmwareBinary(
43+
export async function GetBoardBinaryHashes(
6144
version: string,
6245
board: string,
63-
onProgress: (percent: number) => void
46+
hashType: 'md5' | 'sha256' = 'sha256'
6447
) {
65-
return DownloadBinary(
66-
`https://firmware.openshock.org/${version}/${board}/firmware.bin`,
67-
onProgress
68-
);
48+
const lines = await DownloadLines(boardFileUrl(version, board, `hashes.${hashType}.txt`));
49+
50+
let hashLength: number;
51+
switch (hashType) {
52+
case 'md5':
53+
hashLength = 32;
54+
break;
55+
case 'sha256':
56+
hashLength = 64;
57+
break;
58+
default:
59+
throw new Error(`Unsupported hash type: ${hashType}`);
60+
}
61+
62+
const hashes: Record<string, string> = {};
63+
for (const line of lines) {
64+
const parts = line.split(' ');
65+
if (parts.length < 2) throw new Error(`Invalid hash line: ${line}`);
66+
const hash = parts[0].trim();
67+
if (hash.length !== hashLength) throw new Error(`Invalid hash length in line: ${line}`);
68+
if (!/^[a-f0-9]+$/i.test(hash)) throw new Error(`Invalid hash format in line: ${line}`);
69+
70+
let filename = parts.slice(1).join(' ').trim(); // Join the rest in case filename has spaces
71+
if (!filename) throw new Error(`Invalid filename in line: ${line}`);
72+
73+
if (filename.startsWith('./')) {
74+
// Remove leading './' if present
75+
filename = filename.slice(2);
76+
}
77+
78+
hashes[filename] = hash;
79+
}
80+
81+
return hashes;
6982
}
83+
export async function GetBoardBinaryHash(
84+
version: string,
85+
board: string,
86+
filename: string,
87+
hashType: 'md5' | 'sha256'
88+
) {
89+
if (filename.startsWith('./')) {
90+
filename = filename.slice(2); // Remove leading './' if present
91+
}
7092

71-
export async function GetFirmwareBinaryHash(version: string, board: string) {
72-
const lines = await DownloadLines(
73-
`https://firmware.openshock.org/${version}/${board}/hashes.md5.txt`
74-
);
75-
if (!lines) return null;
93+
const hashes = await GetBoardBinaryHashes(version, board, hashType);
94+
if (filename in hashes) {
95+
return hashes[filename];
96+
}
7697

77-
const hashLine = lines.find((x) => x.endsWith('firmware.bin'));
78-
if (!hashLine) return null;
98+
return null;
99+
}
79100

80-
const hash = hashLine.split(' ')[0].trim();
101+
export async function DownloadAndVerifyBoardBinary(
102+
version: string,
103+
board: string,
104+
filename: string
105+
) {
106+
// Download the binary and its hash in parallel
107+
const [binary, hash] = await Promise.all([
108+
DownloadBinary(boardFileUrl(version, board, filename)),
109+
GetBoardBinaryHash(version, board, filename, 'sha256'),
110+
]);
111+
112+
if (!hash) {
113+
throw new Error(`No hash found for ${filename} in board ${board} version ${version}`);
114+
}
81115

82-
// Validate hash length
83-
if (hash.length != 32) return null;
116+
// Calculate the hash of the downloaded binary
117+
const calculatedHash = await HashBuffer(binary, 'SHA-256');
118+
if (calculatedHash !== hash) {
119+
throw new Error(
120+
`Hash mismatch for ${filename} in board ${board} version ${version}: expected ${hash}, got ${calculatedHash}`
121+
);
122+
}
84123

85-
return hash;
124+
return binary;
86125
}

src/lib/utils/crypto.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ export const ArrayBufferToHex = (buffer: ArrayBuffer) =>
55
.map((num) => num.toString(16).padStart(2, '0'))
66
.join('');
77

8-
export async function HashString(input: string, hashtype: 'SHA-1' | 'SHA-256'): Promise<string> {
9-
const data = EncodeString(input);
10-
const hashBuffer = await crypto.subtle.digest(hashtype, data);
8+
export async function HashBuffer(
9+
input: BufferSource,
10+
hashtype: 'SHA-1' | 'SHA-256'
11+
): Promise<string> {
12+
const hashBuffer = await crypto.subtle.digest(hashtype, input);
1113
return ArrayBufferToHex(hashBuffer);
1214
}
15+
export async function HashString(input: string, hashtype: 'SHA-1' | 'SHA-256') {
16+
return HashBuffer(EncodeString(input), hashtype);
17+
}

src/routes/flashtool/FirmwareFlasher.svelte

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
<script lang="ts">
22
import { Microchip, TriangleAlert } from '@lucide/svelte';
33
import FlashManager from '$lib/EspTool/FlashManager';
4-
import { DownloadFirmwareBinary, GetFirmwareBinaryHash } from '$lib/api/firmwareCDN';
4+
import { DownloadAndVerifyBoardBinary } from '$lib/api/firmwareCDN';
55
import { Button } from '$lib/components/ui/button';
66
import { Progress } from '$lib/components/ui/progress';
7-
import WordArray from 'crypto-js/lib-typedarrays';
8-
import HashMD5 from 'crypto-js/md5';
97
108
interface Props {
119
version: string;
@@ -47,31 +45,13 @@
4745
4846
progressName = 'Downloading firmware...';
4947
progressPercent = undefined;
50-
const firmware = await DownloadFirmwareBinary(version, board, progressCallback);
48+
const firmware = await DownloadAndVerifyBoardBinary(version, board, 'firmware.bin');
5149
if (!firmware) {
5250
progressName = null;
5351
error = 'Failed to download firmware.';
5452
return;
5553
}
5654
57-
progressName = 'Fetching firmware hash...';
58-
progressPercent = undefined;
59-
const firmwareHash = await GetFirmwareBinaryHash(version, board);
60-
if (!firmwareHash) {
61-
progressName = null;
62-
error = 'Failed to get firmware hash.';
63-
return;
64-
}
65-
66-
progressName = 'Verifying firmware hash...';
67-
progressPercent = undefined;
68-
const firmwareHashVerified = HashMD5(WordArray.create(firmware)).toString();
69-
if (firmwareHashVerified !== firmwareHash) {
70-
progressName = null;
71-
error = 'Firmware hash verification failed.';
72-
return;
73-
}
74-
7555
progressName = 'Flashing firmware...';
7656
progressPercent = undefined;
7757
await manager.flash(firmware, eraseBeforeFlash, progressCallback);

0 commit comments

Comments
 (0)