tiger_pkg/
crypto.rs

1use std::collections::HashMap;
2
3use aes_gcm::{AeadInPlace, Aes128Gcm, KeyInit};
4use lazy_static::lazy_static;
5use parking_lot::RwLock;
6use tracing::{error, info};
7
8use crate::{d2_shared::BlockFlags, DestinyVersion, GameVersion, Version};
9
10lazy_static! {
11    static ref CIPHERS_EXTRA: RwLock<HashMap<u64, (Aes128Gcm, [u8; 12])>> = {
12        if let Ok(keyfile) = std::fs::read_to_string("keys.txt") {
13            let k: HashMap<u64, (Aes128Gcm, [u8; 12])> = parse_keys(&keyfile)
14                .into_iter()
15                .map(|(group, key, iv)| (group, (Aes128Gcm::new(&key.into()), iv)))
16                .collect();
17
18            if !k.is_empty() {
19                info!("Loaded {} external keys", k.len());
20            }
21
22            RwLock::new(k)
23        } else {
24            RwLock::new(HashMap::new())
25        }
26    };
27}
28
29pub fn register_pkg_key(group: u64, key: [u8; 16], iv: [u8; 12]) {
30    CIPHERS_EXTRA
31        .write()
32        .insert(group, (Aes128Gcm::new(&key.into()), iv));
33}
34
35pub fn has_pkg_key(group: u64) -> bool {
36    CIPHERS_EXTRA.read().contains_key(&group)
37}
38
39pub struct PkgGcmState {
40    nonce: [u8; 12],
41    cipher_0: Aes128Gcm,
42    cipher_1: Aes128Gcm,
43    cipher_extra: Option<(Aes128Gcm, [u8; 12])>,
44    group: u64,
45}
46
47impl PkgGcmState {
48    pub fn new(pkg_id: u16, version: GameVersion, group: u64) -> PkgGcmState {
49        let mut g = PkgGcmState {
50            nonce: version.aes_nonce_base(),
51            cipher_0: Aes128Gcm::new(&version.aes_key_0().into()),
52            cipher_1: Aes128Gcm::new(&version.aes_key_1().into()),
53            cipher_extra: CIPHERS_EXTRA.read().get(&group).cloned(),
54            group,
55        };
56
57        g.shift_nonce(pkg_id, version);
58
59        g
60    }
61
62    fn shift_nonce(&mut self, pkg_id: u16, version: GameVersion) {
63        match version {
64            GameVersion::Destiny(ver) => {
65                self.nonce[0] ^= (pkg_id >> 8) as u8;
66                match ver {
67                    DestinyVersion::Destiny2Beta | DestinyVersion::Destiny2Shadowkeep => {
68                        self.nonce[1] = 0xf9
69                    }
70                    _ => self.nonce[1] = 0xea,
71                }
72                self.nonce[11] ^= pkg_id as u8;
73            }
74            _ => unimplemented!(),
75        }
76    }
77
78    pub fn decrypt_block_in_place(
79        &self,
80        flags: BlockFlags,
81        tag: &[u8],
82        data: &mut [u8],
83    ) -> anyhow::Result<()> {
84        if flags.contains(BlockFlags::REDACTED) {
85            if let Some((cipher, iv)) = &self.cipher_extra {
86                if cipher
87                    .decrypt_in_place_detached(iv.as_slice().into(), &[], data, tag.into())
88                    .is_ok()
89                {
90                    return Ok(());
91                }
92            }
93
94            return Err(anyhow::anyhow!(format!(
95                "No (working) key found for PKG group {:016X}",
96                self.group
97            )));
98        }
99
100        let (cipher, nonce) = if flags.contains(BlockFlags::ALT_CIPHER) {
101            (&self.cipher_1, &self.nonce)
102        } else {
103            (&self.cipher_0, &self.nonce)
104        };
105
106        match cipher.decrypt_in_place_detached(nonce.into(), &[], data, tag.into()) {
107            Ok(_) => Ok(()),
108            Err(_) => Err(anyhow::anyhow!("Failed to decrypt PKG data block")),
109        }
110    }
111}
112
113// example key `123456789ABCDEF:ABCDA1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D:1234567890ABCDEF // optional comment`
114pub fn parse_keys(data: &str) -> Vec<(u64, [u8; 16], [u8; 12])> {
115    data.lines()
116        .enumerate()
117        .filter_map(|(i, l)| {
118            let mut parts = l.split(':');
119            let Some(group) = parts.next() else {
120                error!("Failed to parse group on line {i}");
121                return None;
122            };
123            let Some(key) = parts.next() else {
124                error!("Failed to parse key on line {i}");
125                return None;
126            };
127            let Some(iv) = parts.next().map(|p| p.chars().take(24).collect::<String>()) else {
128                error!("Failed to parse iv on line {i}");
129                return None;
130            };
131
132            let group = match u64::from_str_radix(group, 16) {
133                Ok(k) => k,
134                Err(e) => {
135                    error!("Failed to parse group on line {i}: {e}");
136                    return None;
137                }
138            };
139
140            let key = match hex::decode(key) {
141                Ok(data) => {
142                    if data.len() != 16 {
143                        error!("Invalid key length on line {i}");
144                        return None;
145                    }
146                    let mut k = [0u8; 16];
147                    k.copy_from_slice(&data);
148                    k
149                }
150                Err(e) => {
151                    error!("Failed to parse key on line {i}: {e}");
152                    return None;
153                }
154            };
155
156            let iv = match hex::decode(iv) {
157                Ok(data) => {
158                    if data.len() != 12 {
159                        error!("Invalid iv length on line {i}");
160                        return None;
161                    }
162                    let mut v = [0u8; 12];
163                    v.copy_from_slice(&data);
164                    v
165                }
166                Err(e) => {
167                    error!("Failed to parse iv on line {i}: {e}");
168                    return None;
169                }
170            };
171
172            Some((group, key, iv))
173        })
174        .collect()
175}