1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! Send an animated (Lottie) sticker, or a sticker pack, to your own number with a
//! saved session (stop the bridge first: one session, one connection).
//! sendsticker <session> <file.was> [premium 0|1]
//! sendsticker <session> --pack <dir> (dir: manifest.tsv, pack.zip, thumb.png)
//! The .was / pack zip goes up byte for byte; see media::lottie_sticker_message and
//! media::sticker_pack_message for the message layouts.
use std::io::Read;
use std::net::TcpStream;
use std::time::Duration;
use inkwhats::wa::client_payload::{self, AppVersion, UserAgent};
use inkwhats::wa::conn::Connection;
use inkwhats::wa::node::{Content, Node};
use inkwhats::wa::noise::KeyPair;
use inkwhats::wa::session::Session;
use inkwhats::wa::signal::{encrypt_first_message_random, PreKeyBundle};
use inkwhats::wa::{media, requests};
const HOST: &str = "g.whatsapp.net:443";
const VER: (u32, u32, u32, u32) = (2, 26, 35, 71);
fn rand32() -> [u8; 32] {
let mut b = [0u8; 32];
std::fs::File::open("/dev/urandom").unwrap().read_exact(&mut b).unwrap();
b
}
fn cbytes(n: &Node, tag: &str) -> Option<Vec<u8>> {
match &n.child(tag)?.content { Content::Bytes(b) => Some(b.clone()), _ => None }
}
fn be(b: &[u8]) -> u32 { b.iter().fold(0u32, |a, &x| (a << 8) | x as u32) }
fn strip05(v: Vec<u8>) -> Option<[u8; 32]> {
if v.len() == 33 && v[0] == 5 { v[1..].try_into().ok() } else { v.try_into().ok() }
}
fn parse_bundle(user: &Node) -> Option<(String, PreKeyBundle)> {
let jid = user.get("jid")?.to_string();
let skey = user.child("skey")?;
let key = user.child("key");
Some((jid, PreKeyBundle {
registration_id: be(&cbytes(user, "registration").unwrap_or_default()),
identity_key: strip05(cbytes(user, "identity")?)?,
signed_prekey_id: be(&cbytes(skey, "id")?),
signed_prekey: strip05(cbytes(skey, "value")?)?,
signed_prekey_sig: cbytes(skey, "signature")?.try_into().ok()?,
prekey_id: key.as_ref().and_then(|k| cbytes(k, "id")).map(|b| be(&b)),
prekey: key.as_ref().and_then(|k| cbytes(k, "value")).and_then(strip05),
}))
}
enum What {
Sticker { was: Vec<u8>, premium: u32 },
Pack { info: media::PackInfo, zip: Vec<u8>, thumb: Vec<u8> },
}
/// Uploads whatever `what` needs and returns the finished Message plus its enc mediatype.
fn build(what: &What, host: &str, auth: &str) -> Result<(Vec<u8>, &'static str), String> {
match what {
What::Sticker { was, premium } => {
let enc = media::encrypt_media(was, media::LABEL_IMAGE, rand32());
let (url, path) = media::upload(host, auth, "sticker", &enc)?;
Ok((media::lottie_sticker_message(&url, &path, &enc, *premium), "sticker"))
}
What::Pack { info, zip, thumb } => {
let key = rand32();
let zip_enc = media::encrypt_media(zip, media::LABEL_STICKER_PACK, key);
let thumb_enc = media::encrypt_media(thumb, media::LABEL_STICKER_PACK_THUMB, key);
let (_, zip_path) = media::upload(host, auth, "sticker-pack", &zip_enc)?;
let (_, thumb_path) = media::upload(host, auth, "thumbnail-sticker-pack", &thumb_enc)?;
Ok((media::sticker_pack_message(info, &zip_enc, &zip_path, &thumb_enc, &thumb_path), "sticker_pack"))
}
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let path = args.get(1).expect("session path");
let what = match (args.get(2).map(String::as_str), args.get(3)) {
(Some("--pack"), Some(dir)) => What::Pack {
info: media::PackInfo::from_manifest(&std::fs::read_to_string(format!("{dir}/manifest.tsv")).expect("manifest.tsv"))
.expect("manifest"),
zip: std::fs::read(format!("{dir}/pack.zip")).expect("pack.zip"),
thumb: std::fs::read(format!("{dir}/thumb.png")).expect("thumb.png"),
},
(Some(file), premium) => What::Sticker {
was: std::fs::read(file).expect("sticker file"),
premium: premium.and_then(|p| p.parse().ok()).unwrap_or(1),
},
_ => panic!("usage: sendsticker <session> <file.was> [premium] | --pack <dir>"),
};
let sess = Session::load(path).expect("session");
let username = sess.username.expect("username");
let device = sess.device.expect("device");
let own_jid = format!("{username}@s.whatsapp.net");
let ua = UserAgent::default_android(
AppVersion { primary: VER.0, secondary: VER.1, tertiary: VER.2, quaternary: VER.3 },
(0..16).map(|_| format!("{:02x}", rand32()[0])).collect(),
);
let payload = client_payload::login(username, device, &ua, "");
let stream = TcpStream::connect(HOST).expect("tcp");
stream.set_read_timeout(Some(Duration::from_secs(25))).unwrap();
stream.set_write_timeout(Some(Duration::from_secs(20))).unwrap();
let mut conn = Connection::connect(stream, sess.keys.noise.clone(), KeyPair::from_secret(rand32()), &payload).expect("handshake");
eprintln!("logged in {username}:{device}");
let mut message: Option<(Vec<u8>, &'static str)> = None; // padded DSM + mediatype
let mut step = 0;
for _ in 0..500 {
let node = match conn.recv_node() { Ok(n) => n, Err(e) => { eprintln!("recv: {e}"); break; } };
if node.tag == "receipt" || node.tag == "message" { continue; }
if node.tag == "success" && step == 0 {
step = 1;
let _ = conn.send_node(&requests::media_conn(&conn.next_id()));
continue;
}
if node.tag == "iq" {
if let Some(mc) = node.child("media_conn") {
let auth = mc.get("auth").unwrap_or("").to_string();
let host = mc.nodes().iter().find(|n| n.tag == "host").and_then(|h| h.get("hostname")).unwrap_or("mmg.whatsapp.net").to_string();
match build(&what, &host, &auth) {
Ok((content, mediatype)) => {
// Own devices (self-send) get it wrapped in a DeviceSentMessage.
let m = media::device_sent_message(&own_jid, &content);
message = Some((media::pad_message(m, 3), mediatype));
let _ = conn.send_node(&requests::get_prekeys(&conn.next_id(), &[own_jid.clone()]));
eprintln!("uploaded; requested prekeys ...");
}
Err(e) => { eprintln!("{e}"); return; }
}
continue;
}
if let Some(list) = node.child("list") {
let Some((msg, mediatype)) = &message else { continue };
let mut encs = Vec::new();
for user in list.nodes().iter().filter(|n| n.tag == "user") {
if let Some((jid, bundle)) = parse_bundle(user) {
if jid.starts_with(&format!("{username}:{device}")) { continue; }
if let Ok(pk) = encrypt_first_message_random(&sess.keys.identity, sess.keys.registration_id, &bundle, msg) {
encs.push(Node::new("to").attr("jid", &jid).children(vec![
Node::new("enc").attr("v", "2").attr("type", "pkmsg").attr("mediatype", *mediatype).bytes(pk)]));
}
}
}
if encs.is_empty() { eprintln!("no targets"); return; }
let id = format!("INKW{}", rand32()[..8].iter().map(|b| format!("{b:02X}")).collect::<String>());
let mut kids = vec![Node::new("participants").children(encs)];
if let Some(adv) = &sess.adv_identity { kids.push(Node::new("device-identity").bytes(adv.clone())); }
let m = Node::new("message").attr("to", &own_jid).attr("id", &id).attr("type", "media").children(kids);
conn.send_node(&m).expect("send");
eprintln!("sent {mediatype} id={id}; waiting for ack ...");
continue;
}
eprintln!("<< iq {:?}", node.attrs);
}
if node.tag == "ack" { eprintln!("ack: {:?}", node.attrs); return; }
}
eprintln!("(done)");
}