1use crate::libsignal::protocol::{KeyPair, PublicKey};
2use aes_gcm::Aes256Gcm;
3use aes_gcm::aead::{Aead, KeyInit, Payload};
4use base64::Engine as _;
5use base64::prelude::*;
6use hkdf::Hkdf;
7use hmac::{Hmac, Mac};
8use prost::Message;
9
10use sha2::Sha256;
11use wacore_binary::builder::NodeBuilder;
12use wacore_binary::jid::{Jid, SERVER_JID};
13use wacore_binary::node::Node;
14use waproto::whatsapp as wa;
15use waproto::whatsapp::AdvEncryptionType;
16
17const ADV_PREFIX_ACCOUNT_SIGNATURE: &[u8] = &[6, 0];
19const ADV_PREFIX_DEVICE_SIGNATURE_GENERATE: &[u8] = &[6, 1];
20const ADV_HOSTED_PREFIX_ACCOUNT_SIGNATURE: &[u8] = &[6, 5];
21const ADV_HOSTED_PREFIX_DEVICE_SIGNATURE_VERIFICATION: &[u8] = &[6, 6];
22
23type HmacSha256 = Hmac<Sha256>;
25
26#[derive(Debug)]
27pub struct PairCryptoError {
28 pub code: u16,
29 pub text: &'static str,
30 pub source: anyhow::Error,
31}
32
33impl std::fmt::Display for PairCryptoError {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(
36 f,
37 "pairing crypto failed with code {}: {} (source: {})",
38 self.code, self.text, self.source
39 )
40 }
41}
42
43impl std::error::Error for PairCryptoError {}
44
45pub struct DeviceState {
47 pub identity_key: KeyPair,
48 pub noise_key: KeyPair,
49 pub adv_secret_key: [u8; 32],
50}
51
52pub struct PairUtils;
54
55impl PairUtils {
56 pub fn make_qr_data(device_state: &DeviceState, ref_str: String) -> String {
58 let noise_b64 =
59 BASE64_STANDARD.encode(device_state.noise_key.public_key.public_key_bytes());
60 let identity_b64 =
61 BASE64_STANDARD.encode(device_state.identity_key.public_key.public_key_bytes());
62 let adv_b64 = BASE64_STANDARD.encode(device_state.adv_secret_key);
63
64 [ref_str, noise_b64, identity_b64, adv_b64].join(",")
65 }
66
67 pub fn build_ack_node(request_node: &Node) -> Option<Node> {
69 if let (Some(to), Some(id)) = (request_node.attrs.get("from"), request_node.attrs.get("id"))
70 {
71 Some(
72 NodeBuilder::new("iq")
73 .attrs([
74 ("to", to.to_string()),
75 ("id", id.to_string()),
76 ("type", "result".to_string()),
77 ])
78 .build(),
79 )
80 } else {
81 None
82 }
83 }
84
85 pub fn build_pair_error_node(req_id: &str, code: u16, text: &str) -> Node {
87 let error_node = NodeBuilder::new("error")
88 .attrs([("code", code.to_string()), ("text", text.to_string())])
89 .build();
90 NodeBuilder::new("iq")
91 .attrs([
92 ("to", SERVER_JID.to_string()),
93 ("type", "error".to_string()),
94 ("id", req_id.to_string()),
95 ])
96 .children([error_node])
97 .build()
98 }
99
100 pub fn do_pair_crypto(
102 device_state: &DeviceState,
103 device_identity_bytes: &[u8],
104 ) -> Result<(Vec<u8>, u32), PairCryptoError> {
105 let hmac_container = wa::AdvSignedDeviceIdentityHmac::decode(device_identity_bytes)
107 .map_err(|e| PairCryptoError {
108 code: 500,
109 text: "internal-error",
110 source: e.into(),
111 })?;
112
113 let is_hosted_account = hmac_container.account_type.is_some()
115 && hmac_container.account_type() == AdvEncryptionType::Hosted;
116
117 let mut mac =
118 <HmacSha256 as Mac>::new_from_slice(&device_state.adv_secret_key).map_err(|e| {
119 PairCryptoError {
120 code: 500,
121 text: "internal-error",
122 source: e.into(),
123 }
124 })?;
125 let details_bytes = hmac_container
127 .details
128 .as_deref()
129 .ok_or_else(|| PairCryptoError {
130 code: 500,
131 text: "internal-error",
132 source: anyhow::anyhow!("HMAC container missing details"),
133 })?;
134 let _hmac_bytes = hmac_container
135 .hmac
136 .as_deref()
137 .ok_or_else(|| PairCryptoError {
138 code: 500,
139 text: "internal-error",
140 source: anyhow::anyhow!("HMAC container missing hmac"),
141 })?;
142
143 if is_hosted_account {
144 mac.update(ADV_HOSTED_PREFIX_ACCOUNT_SIGNATURE);
145 }
146 mac.update(details_bytes);
147 let mut signed_identity =
155 wa::AdvSignedDeviceIdentity::decode(details_bytes).map_err(|e| PairCryptoError {
156 code: 500,
157 text: "internal-error",
158 source: e.into(),
159 })?;
160
161 let account_sig_key_bytes = signed_identity.account_signature_key();
162 let account_sig_bytes = signed_identity.account_signature();
163 let inner_details_bytes = signed_identity.details().to_vec();
164
165 let account_sig_prefix = if is_hosted_account {
166 ADV_HOSTED_PREFIX_ACCOUNT_SIGNATURE
167 } else {
168 ADV_PREFIX_ACCOUNT_SIGNATURE
169 };
170
171 let msg_to_verify = Self::concat_bytes(&[
172 account_sig_prefix,
173 &inner_details_bytes,
174 device_state.identity_key.public_key.public_key_bytes(),
175 ]);
176
177 let account_public_key = PublicKey::from_djb_public_key_bytes(account_sig_key_bytes)
178 .map_err(|e| PairCryptoError {
179 code: 401,
180 text: "invalid-key",
181 source: e.into(),
182 })?;
183
184 if !account_public_key.verify_signature(&msg_to_verify, account_sig_bytes) {
185 return Err(PairCryptoError {
186 code: 401,
187 text: "signature-mismatch",
188 source: anyhow::anyhow!("libsignal signature verification failed"),
189 });
190 }
191
192 let device_sig_prefix = if is_hosted_account {
194 ADV_HOSTED_PREFIX_DEVICE_SIGNATURE_VERIFICATION
195 } else {
196 ADV_PREFIX_DEVICE_SIGNATURE_GENERATE
197 };
198
199 let msg_to_sign = Self::concat_bytes(&[
200 device_sig_prefix,
201 &inner_details_bytes,
202 device_state.identity_key.public_key.public_key_bytes(),
203 account_sig_key_bytes,
204 ]);
205 let device_signature = device_state
206 .identity_key
207 .private_key
208 .calculate_signature(&msg_to_sign, &mut rand::make_rng::<rand::rngs::StdRng>())
209 .map_err(|e| PairCryptoError {
210 code: 500,
211 text: "internal-error",
212 source: e.into(),
213 })?;
214 signed_identity.device_signature = Some(device_signature.to_vec());
215
216 let identity_details =
218 wa::AdvDeviceIdentity::decode(&*inner_details_bytes).map_err(|e| PairCryptoError {
219 code: 500,
220 text: "internal-error",
221 source: e.into(),
222 })?;
223 let key_index = identity_details.key_index();
224
225 let self_signed_identity_bytes = signed_identity.encode_to_vec();
227
228 Ok((self_signed_identity_bytes, key_index))
229 }
230
231 pub fn build_pair_success_response(
233 req_id: &str,
234 self_signed_identity_bytes: Vec<u8>,
235 key_index: u32,
236 ) -> Node {
237 let response_content = NodeBuilder::new("pair-device-sign")
238 .children([NodeBuilder::new("device-identity")
239 .attr("key-index", key_index.to_string())
240 .bytes(self_signed_identity_bytes)
241 .build()])
242 .build();
243 NodeBuilder::new("iq")
244 .attrs([
245 ("to", SERVER_JID.to_string()),
246 ("id", req_id.to_string()),
247 ("type", "result".to_string()),
248 ])
249 .children([response_content])
250 .build()
251 }
252
253 pub fn parse_qr_code(qr_code: &str) -> Result<(String, [u8; 32], [u8; 32]), anyhow::Error> {
255 let parts: Vec<&str> = qr_code.split(',').collect();
256 if parts.len() != 4 {
257 return Err(anyhow::anyhow!("Invalid QR code format"));
258 }
259 let pairing_ref = parts[0].to_string();
260 let dut_noise_pub_b64 = parts[1];
261 let dut_identity_pub_b64 = parts[2];
262 let dut_noise_pub_bytes = BASE64_STANDARD
265 .decode(dut_noise_pub_b64)
266 .map_err(|e| anyhow::anyhow!(e))?;
267 let dut_identity_pub_bytes = BASE64_STANDARD
268 .decode(dut_identity_pub_b64)
269 .map_err(|e| anyhow::anyhow!(e))?;
270
271 let dut_noise_pub: [u8; 32] = dut_noise_pub_bytes
272 .try_into()
273 .map_err(|_| anyhow::anyhow!("Invalid noise public key length"))?;
274 let dut_identity_pub: [u8; 32] = dut_identity_pub_bytes
275 .try_into()
276 .map_err(|_| anyhow::anyhow!("Invalid identity public key length"))?;
277
278 Ok((pairing_ref, dut_noise_pub, dut_identity_pub))
279 }
280
281 pub fn prepare_master_pairing_message(
283 device_state: &DeviceState,
284 pairing_ref: &str,
285 dut_noise_pub: &[u8; 32],
286 dut_identity_pub: &[u8; 32],
287 master_ephemeral: KeyPair,
288 ) -> Result<Vec<u8>, anyhow::Error> {
289 let adv_key = &device_state.adv_secret_key;
291 let identity_key = &device_state.identity_key;
292
293 let mut mac = <HmacSha256 as Mac>::new_from_slice(adv_key)
294 .map_err(|e| anyhow::anyhow!("Failed to init HMAC for master pairing: {e}"))?;
295 mac.update(ADV_PREFIX_ACCOUNT_SIGNATURE);
296 mac.update(dut_identity_pub);
297 mac.update(master_ephemeral.public_key.public_key_bytes());
298 let account_signature = mac.finalize().into_bytes();
299
300 let their_public_key = PublicKey::from_djb_public_key_bytes(dut_noise_pub)?;
301 let shared_secret = master_ephemeral
302 .private_key
303 .calculate_agreement(&their_public_key)?;
304
305 let mut final_message = Vec::new();
306 final_message.extend_from_slice(&account_signature);
307 final_message.extend_from_slice(master_ephemeral.public_key.public_key_bytes());
308 final_message.extend_from_slice(identity_key.public_key.public_key_bytes());
309
310 let encryption_key = {
312 let hk = Hkdf::<Sha256>::new(None, &shared_secret);
313 let mut result = vec![0u8; 32];
314 hk.expand(b"WA-Ads-Key", &mut result)
315 .map_err(|_| anyhow::anyhow!("HKDF expand failed"))?;
316 result
317 };
318 let cipher = Aes256Gcm::new_from_slice(&encryption_key)
319 .map_err(|_| anyhow::anyhow!("Invalid key size for AES-GCM"))?;
320 let nonce: aes_gcm::Nonce<_> = [0u8; 12].into();
321 let payload = Payload {
322 msg: &final_message,
323 aad: pairing_ref.as_bytes(),
324 };
325 let encrypted = cipher
326 .encrypt(&nonce, payload)
327 .map_err(|_| anyhow::anyhow!("AES-GCM encryption failed"))?;
328
329 Ok(encrypted)
330 }
331
332 pub fn build_master_pair_iq(
334 master_jid: &Jid,
335 encrypted_message: Vec<u8>,
336 req_id: String,
337 ) -> Node {
338 let response_content = NodeBuilder::new("pair-device-sign")
339 .attr("jid", master_jid.clone())
340 .bytes(encrypted_message)
341 .build();
342 NodeBuilder::new("iq")
343 .attrs([
344 ("to", SERVER_JID.to_string()),
345 ("type", "set".to_string()),
346 ("id", req_id),
347 ("xmlns", "md".to_string()),
348 ])
349 .children([response_content])
350 .build()
351 }
352
353 fn concat_bytes(slices: &[&[u8]]) -> Vec<u8> {
355 slices.iter().flat_map(|s| s.iter().cloned()).collect()
356 }
357}