cytryna/
crypto.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::marker::PhantomData;
4use std::mem;
5use std::num;
6use std::str::FromStr;
7use std::sync::OnceLock;
8
9use crate::string::SizedCString;
10use crate::{CytrynaError, CytrynaResult, FromBytes};
11
12use sha2::{Digest, Sha256};
13use thiserror::Error;
14
15pub mod aes128_ctr {
16    pub use aes::cipher::block_padding::NoPadding;
17    pub use aes::cipher::BlockDecryptMut;
18    pub use aes::cipher::KeyIvInit;
19    pub use aes::cipher::StreamCipher;
20    pub type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
21    pub type Aes128CtrDec = ctr::Ctr128BE<aes::Aes128>;
22}
23
24static KEY_BAG: OnceLock<KeyBag> = OnceLock::new();
25
26/// Contains keys used for encrypting/decrypting data
27#[derive(Clone, Debug)]
28pub struct KeyBag {
29    keys: HashMap<KeyIndex, [u8; 0x10]>,
30}
31
32impl KeyBag {
33    /// Makes an instance of KeyBag
34    #[must_use]
35    pub fn new() -> Self {
36        Self {
37            keys: HashMap::new(),
38        }
39    }
40    /// Makes an instance of KeyBag from a string in format compatible with
41    /// keys returned by <https://github.com/citra-emu/citra/raw/master/dist/dumpkeys/DumpKeys.gm9>
42    ///
43    /// Note that this script won't dump all keys used in this library
44    #[must_use]
45    pub fn from_string(string: &str) -> CytrynaResult<Self> {
46        let mut this = Self::new();
47        for line in string.lines() {
48            if line.starts_with('#') {
49                continue;
50            }
51            let line = line.to_lowercase();
52            let Some((left, right)) = line.split_once('=') else {
53                continue;
54            };
55            let idx: KeyIndex = left.parse()?;
56            let keyvec = hex::decode(right)?;
57            if keyvec.len() != 0x10 {
58                return Err(CytrynaError::InvalidLength {
59                    what: "key",
60                    actual: keyvec.len(),
61                    expected: 0x10,
62                });
63            }
64            let key: [u8; 0x10] = keyvec.try_into().unwrap();
65
66            this.set_key(idx, key);
67        }
68        Ok(this)
69    }
70    /// Adds a key to KeyBag, overwriting previous data if there was any
71    pub fn set_key(&mut self, idx: KeyIndex, key: [u8; 0x10]) {
72        self.keys.insert(idx, key);
73    }
74    /// Sets the KeyBag to be used for all crypto functions of this crate
75    pub fn finalize(self) {
76        let _ = KEY_BAG.set(self);
77    }
78    /// Returns a key if it is contained in global KeyBag instance
79    pub fn get_key(&self, idx: KeyIndex) -> CytrynaResult<&[u8; 0x10]> {
80        self.keys.get(&idx).ok_or(CytrynaError::MissingKey(idx))
81    }
82    /// Returns reference to the global KeyBag instance
83    pub fn global() -> CytrynaResult<&'static Self> {
84        KEY_BAG.get().ok_or(CytrynaError::NoKeyBag)
85    }
86}
87
88/// Generates a normal-key from X and Y keys and a keygen constant
89#[must_use]
90pub fn keygen(x: [u8; 0x10], y: [u8; 0x10]) -> CytrynaResult<[u8; 0x10]> {
91    let x = u128::from_be_bytes(x);
92    let y = u128::from_be_bytes(y);
93    let gen = u128::from_be_bytes(*KeyBag::global()?.get_key(KeyIndex::Generator)?);
94
95    Ok(((x.rotate_left(2) ^ y).wrapping_add(gen))
96        .rotate_right(41)
97        .to_be_bytes())
98}
99
100/// Is this self-documenting? I think it is
101#[derive(Debug, Clone, Eq, Hash, PartialEq)]
102pub enum KeyIndex {
103    /// The generator key
104    Generator,
105    /// Key contained in 3DS's AES slot
106    Slot(u8, KeyType),
107    /// Index for common keyY used in Title Key decryption
108    Common(u8),
109    /// Index for common normal-key used in Title Key decryption
110    CommonN(u8),
111}
112
113impl fmt::Display for KeyIndex {
114    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115        let string = match self {
116            Self::Generator => "generator".to_string(),
117            Self::Slot(num, ty) => format!("slot0x{num:X}Key{ty}"),
118            Self::Common(num) => format!("common{num}"),
119            Self::CommonN(num) => format!("common{num}N"),
120        };
121        f.write_str(&string)
122    }
123}
124
125/// An error type for KeyIndex parsing
126#[derive(Error, Debug)]
127pub enum KeyIndexParseError {
128    #[error("Failed to parse a hex number")]
129    NumberParseError(#[from] num::ParseIntError),
130    #[error("Invalid key type \"{0}\"")]
131    InvalidKeyType(String),
132    #[error("Invalid X/Y/Z key type \"{0}\"")]
133    InvalidKeyXYNType(String),
134}
135
136impl FromStr for KeyIndex {
137    type Err = KeyIndexParseError;
138
139    fn from_str(from: &str) -> Result<Self, KeyIndexParseError> {
140        if from == "generator" {
141            Ok(Self::Generator)
142        } else if from.starts_with("slot") {
143            let from = from.trim_start_matches("slot").trim_start_matches("0x");
144            let num = u8::from_str_radix(&from[..2], 16)?;
145            let keytype = match &from[2..] {
146                "keyx" => KeyType::X,
147                "keyy" => KeyType::Y,
148                "keyn" => KeyType::N,
149                _ => return Err(KeyIndexParseError::InvalidKeyXYNType(from[2..].to_string())),
150            };
151            Ok(Self::Slot(num, keytype))
152        } else if from.starts_with("common") {
153            let from = from.trim_start_matches("common");
154            let num = u8::from_str_radix(from.get(0..1).unwrap(), 16)?;
155            let has_n = from.ends_with('n');
156            if has_n {
157                Ok(Self::Common(num))
158            } else {
159                Ok(Self::CommonN(num))
160            }
161        } else {
162            Err(KeyIndexParseError::InvalidKeyType(from.to_string()))
163        }
164    }
165}
166
167/// Type of a 3DS key
168#[derive(Debug, Clone, Eq, Hash, PartialEq)]
169pub enum KeyType {
170    /// KeyX
171    X,
172    /// KeyY
173    Y,
174    /// Normal-key
175    N,
176}
177
178impl fmt::Display for KeyType {
179    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
180        let string = match self {
181            Self::X => "X",
182            Self::Y => "Y",
183            Self::N => "N",
184        };
185        f.write_str(string)
186    }
187}
188
189/// Computes sha data of a byte slice
190pub fn sha256(data: &[u8]) -> [u8; 0x20] {
191    let mut hasher = Sha256::new();
192    hasher.update(data);
193    hasher.finalize().into()
194}
195
196/// Contains signed data in a way that is generic over signed data type and signature type
197#[repr(C)]
198pub struct SignedDataInner<T: ?Sized + FromBytes + fmt::Debug, S: Signature> {
199    _unused: PhantomData<T>,
200    sig_type: SignatureType,
201    signature: S,
202    sig_issuer: SizedCString<0x40>,
203    data: [u8],
204}
205
206impl<T: ?Sized + FromBytes + fmt::Debug, S: Signature> SignedDataInner<T, S> {
207    /// Returns stored data
208    #[must_use]
209    pub fn data(&self) -> &T {
210        T::cast(&self.data)
211    }
212}
213
214impl<T, S> fmt::Debug for SignedDataInner<T, S>
215where
216    T: ?Sized + FromBytes + fmt::Debug,
217    S: Signature,
218{
219    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
220        f.debug_struct("SignedDataInner")
221            .field("sig_type", &self.sig_type)
222            .field("signature", &"skipped")
223            .field("sig_issuer", &self.sig_issuer)
224            .field("data", &T::cast(&self.data))
225            .finish()
226    }
227}
228
229/// Stores SignedDataInner in a way that makes it possible to use as a return type of a function
230pub enum SignedData<'a, T: ?Sized + FromBytes + fmt::Debug> {
231    Rsa4096Sha256(&'a SignedDataInner<T, Rsa4096Sha256>),
232    Rsa2048Sha256(&'a SignedDataInner<T, Rsa2048Sha256>),
233    EcdsaSha256(&'a SignedDataInner<T, EcdsaSha256>),
234}
235
236impl<T> fmt::Debug for SignedData<'_, T>
237where
238    T: ?Sized + FromBytes + fmt::Debug,
239{
240    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
241        match self {
242            Self::Rsa4096Sha256(inner) => f.debug_tuple("Rsa4096Sha256").field(inner).finish(),
243            Self::Rsa2048Sha256(inner) => f.debug_tuple("Rsa2048Sha256").field(inner).finish(),
244            Self::EcdsaSha256(inner) => f.debug_tuple("EcdsaSha256").field(inner).finish(),
245        }
246    }
247}
248
249impl<T: ?Sized + FromBytes + fmt::Debug> SignedData<'_, T> {
250    #[must_use]
251    pub fn from_bytes(bytes: &[u8]) -> CytrynaResult<SignedData<T>> {
252        unsafe {
253            if bytes[0] != 0x0
254                || bytes[1] != 0x1
255                || bytes[2] != 0x0
256                || bytes[3] >= 0x06
257                || bytes[3] <= 0x02
258            {
259                return Err(CytrynaError::InvalidMagic);
260            }
261
262            let sig_size = match bytes[3] {
263                0x03 => mem::size_of::<Rsa4096Sha256>(),
264                0x04 => mem::size_of::<Rsa2048Sha256>(),
265                0x05 => mem::size_of::<EcdsaSha256>(),
266                _ => unreachable!("Already checked if it's in range"),
267            };
268            let offset = sig_size + mem::size_of::<SignatureType>() + 0x40;
269
270            T::bytes_ok(&bytes[offset..])?;
271
272            match bytes[3] {
273                0x03 => Ok(SignedData::Rsa4096Sha256(mem::transmute(bytes))),
274                0x04 => Ok(SignedData::Rsa2048Sha256(mem::transmute(bytes))),
275                0x05 => Ok(SignedData::EcdsaSha256(mem::transmute(bytes))),
276                _ => unreachable!("Already checked if it's in range"),
277            }
278        }
279    }
280    /// Returns a reference to data stored inside
281    #[must_use]
282    pub fn data(&self) -> &T {
283        match self {
284            Self::Rsa4096Sha256(inner) => T::cast(&inner.data),
285            Self::Rsa2048Sha256(inner) => T::cast(&inner.data),
286            Self::EcdsaSha256(inner) => T::cast(&inner.data),
287        }
288    }
289}
290
291/// Stores signature type of TMD and Ticket structs in a little-endian way
292#[derive(Copy, Clone, Debug)]
293#[repr(u32)]
294pub enum SignatureType {
295    Rsa4096Sha256 = 0x03000100,
296    Rsa2048Sha256 = 0x04000100,
297    EcdsaSha256 = 0x05000100,
298}
299
300pub trait Signature: sealed_impl::Sealed {}
301
302/// RSA_4096 SHA256 signature data, including padding
303#[repr(C, packed)]
304pub struct Rsa4096Sha256 {
305    sig: [u8; 0x200],
306    pad: [u8; 0x3c],
307}
308impl Signature for Rsa4096Sha256 {}
309
310/// RSA_2048 SHA256 signature data, including padding
311#[repr(C, packed)]
312pub struct Rsa2048Sha256 {
313    sig: [u8; 0x100],
314    pad: [u8; 0x3c],
315}
316impl Signature for Rsa2048Sha256 {}
317
318/// ECDSA with SHA256 signature data, including padding
319#[repr(C, packed)]
320pub struct EcdsaSha256 {
321    sig: [u8; 0x3c],
322    pad: [u8; 0x40],
323}
324impl Signature for EcdsaSha256 {}
325
326mod sealed_impl {
327    pub trait Sealed {}
328    impl Sealed for super::Rsa4096Sha256 {}
329    impl Sealed for super::Rsa2048Sha256 {}
330    impl Sealed for super::EcdsaSha256 {}
331}
332
333#[cfg(test)]
334mod tests {
335    use super::{KeyBag, KeyIndex};
336    #[test]
337    fn test_keygen() {
338        // https://www.random.org/cgi-bin/randbyte?nbytes=16&format=h
339        const RANDOM_GENERATOR: [u8; 0x10] = [
340            0x12, 0x59, 0x9a, 0x14, 0xff, 0x66, 0xda, 0x9f, 0x65, 0xc1, 0x3e, 0xad, 0x30, 0x50,
341            0x15, 0xc7,
342        ];
343        const RANDOM_X: [u8; 0x10] = [
344            0xfa, 0xfe, 0x20, 0x7b, 0xb2, 0x3c, 0xa4, 0x30, 0x16, 0x2a, 0x65, 0xf6, 0xd3, 0xff,
345            0x50, 0x40,
346        ];
347        const RANDOM_Y: [u8; 0x10] = [
348            0x82, 0x48, 0x62, 0xde, 0xd5, 0xc6, 0xd5, 0x99, 0x23, 0x05, 0x19, 0xf5, 0x2d, 0x27,
349            0x56, 0xa8,
350        ];
351        const REFERENCE_KEY: [u8; 0x10] = [
352            0x6d, 0xc9, 0x95, 0x16, 0xb9, 0x3e, 0x05, 0x3e, 0xa2, 0x8e, 0x4d, 0x8f, 0xfc, 0x70,
353            0xb6, 0xe6,
354        ];
355
356        let mut bag = KeyBag::new();
357        bag.set_key(KeyIndex::Generator, RANDOM_GENERATOR);
358        bag.finalize();
359
360        assert_eq!(super::keygen(RANDOM_X, RANDOM_Y).unwrap(), REFERENCE_KEY);
361    }
362}