tx_custom_boot/
lib.rs

1#![warn(missing_docs)]
2//! crate to generate a boot.dat for sx pro from a payload for the switch
3
4use binwrite::{BinWrite, WriterOption};
5use conv::ValueFrom;
6use sha2::{Digest, Sha256};
7use std::fmt;
8use std::fmt::Formatter;
9use std::io::Write;
10use thiserror::Error;
11
12#[derive(BinWrite, Debug, Default)]
13#[binwrite(little)]
14/// boot.dat header
15// typedef struct boot_dat_hdr
16// {
17//     unsigned char ident[0x10];
18//     unsigned char sha2_s2[0x20];
19//     unsigned int s2_dst;
20//     unsigned int s2_size;
21//     unsigned int s2_enc;
22//     unsigned char pad[0x10];
23//     unsigned int s3_size;
24//     unsigned char pad2[0x90];
25//     unsigned char sha2_hdr[0x20];
26// } boot_dat_hdr_t;
27struct BootDatHeader {
28    inner: BootDatInner,
29    sha2_hdr: Sha2,
30}
31
32#[derive(BinWrite, Debug, Default)]
33#[binwrite(little)]
34struct BootDatInner {
35    ident: [u8; 0xc],
36    vers: [u8; 0x4],
37    sha2_s2: Sha2,
38    s2_dst: u32,
39    s2_size: u32,
40    s2_enc: u32,
41    pad: [u8; 0x10],
42    s3_size: u32,
43    pad2: Pad2,
44}
45
46/// Error enum for this crate
47#[derive(Error, Debug, Clone)]
48pub enum Error {
49    /// Error during I/O operations
50    IoError(String),
51    /// Error while converting the hash
52    HashError,
53    /// Error while truncating lengths
54    TruncationError,
55}
56
57impl std::convert::From<std::io::Error> for Error {
58    fn from(err: std::io::Error) -> Self {
59        Error::IoError(err.to_string())
60    }
61}
62
63impl std::fmt::Display for Error {
64    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
65        match self {
66            Error::IoError(s) => write!(fmt, "IO Error: {}", s),
67            Error::HashError => write!(fmt, "Hash Error"),
68            Error::TruncationError => write!(fmt, "Number Truncation Error"),
69        }
70    }
71}
72
73// Workaround because Default and BinWrite don't support arrays of this dimension
74struct Pad2([u8; 0x90]);
75
76impl Default for Pad2 {
77    fn default() -> Self {
78        Pad2([0; 0x90])
79    }
80}
81
82impl fmt::Debug for Pad2 {
83    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
84        write!(f, "{:?}", self.0)
85    }
86}
87
88impl BinWrite for Pad2 {
89    fn write_options<W: Write>(
90        &self,
91        writer: &mut W,
92        options: &WriterOption,
93    ) -> std::io::Result<()> {
94        for item in &self.0 {
95            BinWrite::write_options(item, writer, options)?;
96        }
97        Ok(())
98    }
99}
100
101#[derive(Default)]
102struct Sha2([u8; 0x20]);
103
104impl fmt::Debug for Sha2 {
105    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
106        write!(f, "{:?}", self.0)
107    }
108}
109
110impl BinWrite for Sha2 {
111    fn write_options<W: Write>(
112        &self,
113        writer: &mut W,
114        options: &WriterOption,
115    ) -> std::io::Result<()> {
116        for item in &self.0 {
117            BinWrite::write_options(item, writer, options)?;
118        }
119        Ok(())
120    }
121}
122
123/// Get the crate version
124#[must_use]
125pub fn get_version() -> &'static str {
126    env!("CARGO_PKG_VERSION")
127}
128
129/// generate a boot.dat given a payload
130/// from <https://gist.github.com/CTCaer/13c02c05daec9e674ba00ce5ac35f5be>
131/// but revisited to match <https://sx-boot-dat-creator.herokuapp.com/> which works for me
132/// `payload` is a byte array of the payload
133///
134/// # Errors
135/// Returns an Error if there are problem hashing or serializing
136pub fn generate_boot_dat(payload: &[u8]) -> Result<Vec<u8>, Error> {
137    let mut header = BootDatHeader::default();
138    header.inner.ident = [
139        0x49, 0x6e, 0x73, 0x61, 0x6e, 0x65, 0x20, 0x42, 0x4F, 0x4F, 0x54, 0x00,
140    ];
141    header.inner.vers = [0x56, 0x31, 0x2E, 0x30];
142
143    let stage_2_sha256 = sha256_digest(payload);
144    header.inner.sha2_s2 = Sha2(stage_2_sha256.try_into().map_err(|_| Error::HashError)?);
145    header.inner.s2_dst = 0x4001_0000;
146    header.inner.s2_size = u32::value_from(payload.len()).map_err(|_| Error::TruncationError)?;
147
148    let mut inner_serialized = vec![];
149    header.inner.write(&mut inner_serialized)?;
150
151    let header_inner_sha256 = sha256_digest(inner_serialized.as_slice());
152    header.sha2_hdr = Sha2(
153        header_inner_sha256
154            .try_into()
155            .map_err(|_| Error::HashError)?,
156    );
157
158    let mut serialized = vec![];
159    header.write(&mut serialized)?;
160
161    serialized.extend_from_slice(payload);
162    Ok(serialized)
163}
164
165/// Calc sha256 for a byte array
166fn sha256_digest(to_hash: &[u8]) -> Vec<u8> {
167    let mut hasher = Sha256::new();
168    hasher.update(to_hash);
169    hasher.finalize().to_vec()
170}
171
172#[cfg(test)]
173mod tests {
174    use hex_literal::hex;
175    #[test]
176    fn end_to_end_test() {
177        let generated = super::generate_boot_dat(&[0xa, 0xb, 0xc]).unwrap();
178        let hash = super::sha256_digest(generated.as_slice());
179        assert_eq!(
180            hash[..],
181            hex!("6ce4c88e604d351b0e14bca7dbf135b3c8c44428718b704883599f285eed984e")
182        );
183    }
184}