whatsapp: M1 foundations

This commit is contained in:
2026-10-28 03:42:33 +01:00
parent 60c18bd4a1
commit 499c6c5db6
30 changed files with 1635 additions and 90 deletions
+1
View File
@@ -2,6 +2,7 @@
## [Unreleased] - 2026-10-28
- WhatsApp transpilation harness (M0): whatsmeow `v0.0.0-20261027104441-1eec1136a50b` (commit `1eec1136a50b...0457`) pinned in [docs/whatsapp.md](docs/whatsapp.md) section 17; `tools/wa-api-inventory.sh` extracts the mechanical scope (1500 entries: every exported symbol in non-test, non-generated Go source plus the 129 used protobuf messages), `tools/wa-mapping.tsv` maps each entry to its `detz::wa` counterpart or `UNMAPPED`, `tools/wa-coverage.sh` fails below 100% (baseline 137/1500, 9.1%), `tools/wa-stub-scan.sh` fails on stub patterns and new silent early-outs. Section 17 also opens the moloapp reuse table.
- WhatsApp foundations (M1, [docs/whatsapp.md](docs/whatsapp.md) section 18): token-dict freshness oracle (236 + 4x256 strings verified verbatim), socket keepalive pings (`WaKeepAlive`, 20-30 s jitter, 10 s deadline, 3 min horizon), full iq request/response (`WaIq` + `WaIqError`/`WaDisconnectedError` taxonomy, whatsmeow-shaped request ids, 75 s default wait), Noise parity (`WaNoise::mix_shared_secret`, overflow-safe framing, `WaServerCert` chain verification that caught and fixed two proto field bugs, socket guards). Slice `tools/wa-slices/m1-foundations.txt` at 100% (91/91); full inventory 210/1522. Live-verified: handshake + cert check pass against web.whatsapp.com, pings answered over a 60 s session.
## [1.46.1] - 2026-10-27
+111
View File
@@ -800,3 +800,114 @@ fails review.
| 2026-10-28 | WebSocket client transport | `realtime/websocket/client/websocket_client.h` (exists, but needs a caller-provided transport: the caller does TLS and the HTTP handshake itself) | keep local `WaSocket` on libcurl: live-verified against the real server (connect + frames), libcurl already linked. Revisit if libcurl ws proves insufficient (e.g. M1 keepalive pings). |
| 2026-10-28 | media HTTPS upload/download | `clients/` HTTP client, pool, proxy (heavier connection-pool stack) | keep local `WaMedia` on `curl_easy` (`wa_media.cpp`): two calls (one POST, one GET), libcurl already linked |
| 2026-10-28 | vault storage for device/sessions/contacts | detz-owned `store::SqlDb` (already the vault; the schema is detz's) | keep local `WaStore` on `store::SqlDb` (`wa_store.h`); no moloapp store facility needed |
| 2026-10-28 | M1 keepalive pings (revisit of the M0 transport row) | `realtime/websocket/client/websocket_client.h` `send_ping`, `realtime/websocket/server/heartbeat_manager.h`, `realtime/engineio/heartbeat_scheduler.h` | wrote local `WaKeepAlive` (`wa_keepalive.h`): whatsmeow keepalive is an application iq (`w:p` get), not a websocket ping frame, with whatsmeow-specific timing (20-30 s jitter, 10 s deadline, 3 min horizon); moloapp's facilities are ws-protocol/engine.io level and none fits. `WaSocket` stays on libcurl. |
## 18. M1 foundations: tokens, socket, iq, Noise (2026-10-28)
Milestone M1 of `full_transpilation_and_integration.md` (slice
`tools/wa-slices/m1-foundations.txt`, 91 entries, 100% mapped; full inventory
210/1522, 13.8%). Four pieces, each proven by a Go oracle that calls the pinned
whatsmeow itself, plus hostile-input tests and a sanitizer run.
### 18.1 Harness repairs found by M1
Three harness repairs surfaced and were fixed in this milestone (all minimal,
all required by the M1 gates):
- `tools/wa-api-inventory.sh` cut `//` comments before stripping string
literals, so the `//` inside the `"https://..."` token broke brace-depth
counting and the `var (` block in `binary/token/token.go` swallowed the
rest of the file. `token.DictVersion`, 3 lookup funcs and 18 wire consts
(22 entries) were missing from the scope. Fixed by stripping strings
first; the inventory is now 1522 entries (was 1500) with zero removals.
- `tools/wa-stub-scan.sh` died silently (exit 1, no verdict) on untracked
files without matches: `grep` exits 1, and `set -e`/`pipefail` killed the
script. Fixed with `|| true` plus a comment; M0's validation (fail on an
injected TODO / silent return) still holds.
- `tools/san-whatsapp.sh` compiled only 6 parser TUs, but the test now links
the whole module. It compiles all of `src/whatsapp/` plus the support TUs
(`json`, `seal_base64`, `sqldb`, `fuzzy`, `qrcodegen`) and links curl and
sqlite3.
### 18.2 Token dictionary freshness
`tests/whatsapp_data/token_oracle.go` dumps whatsmeow's own `binary/token`
tables (version, 236 + 4x256 strings, 18 consts) and whatsmeow-marshaled wire
bytes for 14 token-probe nodes (single-byte tokens, one token per double-byte
dictionary, an untokenized literal). `build/whatsapp_test` (`test_tokens`)
compares every string verbatim, every const, and byte-exact re-encodes all 14
probes. Re-running the oracle after a pin move fails loudly on drift. The
tables were already correct; the check is the deliverable.
Two encoding forms differ cosmetically and both are valid: whatsmeow writes a
typed server-JID `to` as JID_PAIR while detz writes the same string as its
1-byte token, and whatsmeow writes `""` as NIBBLE_8 while detz writes
BINARY_8. Both pairs decode identically (proven in `test_iq`/`test_tokens`);
the server accepts ours (proven live, section 18.5).
### 18.3 Socket keepalive pings
`WaKeepAlive` (`wa_keepalive.h`) is the whatsmeow `keepAliveLoop` schedule as
pure timing state over an injected millisecond clock: jittered 20-30 s
intervals, error counting, forced reconnect past the 3 min horizon. Caps in
`wa_consts.h` (`kWaKeepAliveResponseDeadline` 10 s, `kWaKeepAliveIntervalMin`
20 s, `kWaKeepAliveIntervalMax` 30 s, `kWaKeepAliveMaxFailTime` 180 s).
`WaClient::run_until` starts it and sends a `w:p` get on schedule via
`query_iq`; any reply proves liveness, exactly as `sendKeepAlive`. On a forced
reconnect the client logs and disconnects; the redial lands with M9
auto-reconnect. The `KeepAliveTimeout`/`KeepAliveRestored` events belong to
the M9 event bus; until then failures are loud log lines.
### 18.4 Full iq request/response
`WaIq` (`wa_iq.h`) mirrors `request.go` for the synchronous client: `WaIqQuery`
plus `build` (id/xmlns/type, `smax_id`, `to`, `target`, one child),
`parse_error` (whatsmeow `parseIQError`, overflow-safe code parse, never
fails), `is_disconnect_node` / `is_auth_error_disconnect` (401, replaced,
device_removed), and whatsmeow-shaped request ids (`new_prefix` draws
`<byte0>.<byte1>-`, `generate_id` appends the counter). `WaIqError` carries
the 13 `ErrIQ*` sentinels plus `timed_out()` with `message()`/`is()`;
`WaDisconnectedError` carries `for_iq()` with `message()`/`is()`.
`WaClient::send_iq` now classifies every reply (error replies log code/text
and are returned; disconnects and timeouts log loudly), defaults to the 75 s
`kWaIqDefaultTimeout`, and `query_iq` fills ids. `ErrClientIsNil` maps to the
class itself: methods take references, a nil receiver is unrepresentable.
Retry-on-reconnect needs M9 auto-reconnect (`NoRetry` arrives with it); today
a disconnect during an iq is a loud `WaDisconnectedError`, never a silent
drop. Vectors: `iq_oracle.go` (whatsmeow-marshaled ping, count, 404, 500,
bare, result, three disconnects; code/text extracted by whatsmeow's own
getters; byte-exact encode checks where deterministic).
### 18.5 Noise parity
- `WaNoise::mix_shared_secret` names the X25519-into-key step
(`MixSharedSecretIntoKey`); the handshake calls it three times. Behavior is
unchanged (existing transcript test) and now differentially proven: the
oracle runs whatsmeow's own `NoiseHandshake` over the wa-go trace and our
decrypted server static + payload match byte-for-byte.
- `WaFraming::kLengthSize` names the 3-byte prefix, and `unframe` uses
overflow-safe subtraction checks (it had the forbidden `pos + 3` form).
- `WaServerCert` (`wa_cert.h`) verifies the Noise certificate chain exactly
as `verifyServerCert`: pinned-key intermediate check, issuer serials,
64-byte signatures, key lengths, validity windows, leaf-key-vs-static
match. Any failure throws `WaCertError` with the reason, and the handshake
fails closed. This caught two real proto bugs: `parse_cert_chain` had leaf
and intermediate swapped (fields are leaf = 1, intermediate = 2), and
`parse_cert_details` read the wrong fields (serial = 1, issuer = 2, key =
3, not_before = 4, not_after = 5). Vectors: `cert_oracle.go` (decrypted
bytes, per-field values, per-check verdicts from whatsmeow + libsignal;
the captured leaf is expired today, so the test time-travels inside its
window for the accept case and asserts expiry now, plus 6 tamper and 4
hostile rejections).
- `WaSocket` gained the already-open and not-connected guards plus
`WaSocketStatusError` (HTTP status on connect failure); `WaClient`
refuses a second connect loudly and reports closes through the new
`on_disconnect` hook.
Live verification (2026-10-28, scratch `TAI_HOME`, no phone):
`detz --whatsapp-link` completed the Noise handshake against
`web.whatsapp.com` with the new certificate verification passing
("handshake ok (registration)") and rendered the QR. A 60 s session fired
keepalive pings on schedule with server replies (zero unanswered/failed
lines, connection stayed open until the timeout kill). The cert accept path
and the ping shape are therefore live-proven, not just oracle-proven.
+1
View File
@@ -48,6 +48,7 @@ class WaBinary {
NIBBLE_8 = 255,
PACKED_MAX = 127,
};
static constexpr int SINGLE_BYTE_MAX = 256; // token.SingleByteMax: single-byte token indexes run 0..255
};
} // namespace detz::wa
+81
View File
@@ -0,0 +1,81 @@
#include "wa_cert.h"
#include <array>
#include <cstdint>
#include <cstring>
#include "wa_consts.h"
#include "wa_crypto.h"
#include "wa_proto.h"
namespace detz::wa {
namespace {
std::array<uint8_t, 32> as32(const Bytes& b) {
std::array<uint8_t, 32> a{};
std::memcpy(a.data(), b.data(), 32);
return a;
}
std::array<uint8_t, 64> as64(const Bytes& b) {
std::array<uint8_t, 64> a{};
std::memcpy(a.data(), b.data(), 64);
return a;
}
// Range-checked validity (checkCertValidity): a negative now or a window edge
// outside int64 is rejected rather than converted.
void check_validity(const WaNoiseCertDetails& d, const std::string& which, int64_t now_unix) {
const bool too_early = now_unix < 0 || static_cast<uint64_t>(now_unix) < d.not_before;
if (too_early) throw WaCertError(which + " certificate not valid yet");
const bool too_late = d.not_after > static_cast<uint64_t>(INT64_MAX) ||
now_unix > static_cast<int64_t>(d.not_after);
if (too_late) throw WaCertError(which + " certificate expired");
}
void verify_inner(const Bytes& chain, const Bytes& server_static, int64_t now_unix) {
auto parsed = parse_cert_chain(chain);
if (!parsed) throw WaCertError("failed to unmarshal noise certificate");
const Bytes& im_raw = parsed->intermediate_details;
const Bytes& im_sig = parsed->intermediate_signature;
const Bytes& leaf_raw = parsed->leaf_details;
const Bytes& leaf_sig = parsed->leaf_signature;
if (im_raw.empty() || im_sig.empty() || leaf_raw.empty() || leaf_sig.empty())
throw WaCertError("missing parts of noise certificate");
if (im_sig.size() != 64)
throw WaCertError("unexpected length of intermediate cert signature " + std::to_string(im_sig.size()));
if (leaf_sig.size() != 64)
throw WaCertError("unexpected length of leaf cert signature " + std::to_string(leaf_sig.size()));
if (!WaCrypto::xeddsa_verify(WaConsts::kWaCertPubKey, im_raw, as64(im_sig)))
throw WaCertError("failed to verify intermediate cert signature");
auto im = parse_cert_details(im_raw);
if (!im) throw WaCertError("failed to unmarshal intermediate certificate details");
if (im->issuer_serial != WaConsts::kWaCertIssuerSerial)
throw WaCertError("unexpected intermediate issuer serial " + std::to_string(im->issuer_serial));
if (im->key.size() != 32)
throw WaCertError("unexpected length of intermediate cert key " + std::to_string(im->key.size()));
if (!WaCrypto::xeddsa_verify(as32(im->key), leaf_raw, as64(leaf_sig)))
throw WaCertError("failed to verify leaf cert signature");
check_validity(*im, "intermediate", now_unix);
auto leaf = parse_cert_details(leaf_raw);
if (!leaf) throw WaCertError("failed to unmarshal leaf certificate details");
if (leaf->issuer_serial != im->serial)
throw WaCertError("unexpected leaf issuer serial " + std::to_string(leaf->issuer_serial));
if (!WaCrypto::const_time_eq(leaf->key, server_static)) throw WaCertError("cert key doesn't match decrypted static");
check_validity(*leaf, "leaf", now_unix);
}
} // namespace
void WaServerCert::verify(const Bytes& chain, const Bytes& server_static, int64_t now_unix) {
try {
verify_inner(chain, server_static, now_unix);
} catch (const WaCertError&) {
throw;
} catch (const std::exception& e) {
throw WaCertError(std::string("unreadable certificate chain: ") + e.what());
}
}
} // namespace detz::wa
+28
View File
@@ -0,0 +1,28 @@
#pragma once
// WaServerCert: verification of the WhatsApp server's Noise certificate chain
// against the pinned WACertPubKey, exactly as whatsmeow verifyServerCert
// (handshake.go, MPL-2.0). A failed check throws naming the reason; the
// handshake must fail closed. See docs/whatsapp.md section 18.
#include <cstdint>
#include <ctime>
#include <stdexcept>
#include <string>
#include "wa_node.h"
namespace detz::wa {
class WaCertError : public std::runtime_error {
public:
explicit WaCertError(const std::string& m) : std::runtime_error("wa cert: " + m) {}
};
class WaServerCert {
public:
// Verify the decrypted Noise certificate payload (the CertChain) against
// the decrypted server static key. now_unix defaults to the current time;
// tests inject it to travel inside a captured chain's validity window.
static void verify(const Bytes& chain, const Bytes& server_static, int64_t now_unix = std::time(nullptr));
};
} // namespace detz::wa
+71 -6
View File
@@ -1,7 +1,5 @@
#include "wa_client.h"
#include <unistd.h>
#include <algorithm>
#include <chrono>
#include <cstdlib>
@@ -12,6 +10,7 @@
#include "seal_base64.h"
#include "wa_appstate.h"
#include "wa_binary.h"
#include "wa_cert.h"
#include "wa_consts.h"
#include "wa_media.h"
#include "wa_proto.h"
@@ -34,7 +33,7 @@ constexpr std::array<uint8_t, 2> kAdvHostedAccountPrefix = {6, 5};
constexpr int kAdvEncryptionHosted = 1; // ADVEncryptionType_HOSTED
} // namespace
WaClient::WaClient(WaStorePort& store) : store_(store) {
WaClient::WaClient(WaStorePort& store) : store_(store), iq_prefix_(WaIq::new_prefix()) {
if (auto d = store_.load_device()) device_ = *d;
else {
device_ = WaDevice::generate();
@@ -48,9 +47,13 @@ void WaClient::log(const std::string& s) {
if (on_log) on_log(s);
}
std::string WaClient::next_iq_id() { return std::to_string(++iq_counter_) + "." + std::to_string(::getpid() & 0xffff); }
std::string WaClient::next_iq_id() { return WaIq::generate_id(iq_prefix_, ++iq_counter_); }
bool WaClient::connect() {
if (sock_ && sock_->connected()) {
log("connect refused: already connected");
return false;
}
sock_ = std::make_unique<WaSocket>();
try {
sock_->connect(WaConsts::kWaConnectTimeout);
@@ -82,6 +85,12 @@ bool WaClient::do_handshake() {
log("noise handshake failed");
return false;
}
try {
WaServerCert::verify(res->server_payload, res->server_static);
} catch (const std::exception& e) {
log(std::string("server certificate rejected, failing closed: ") + e.what());
return false;
}
try {
sock_->send_frame(WaFraming::frame(res->client_finish, false));
} catch (const std::exception& e) {
@@ -314,11 +323,27 @@ void WaClient::handle_pair_success(const WaNode& iq) {
}
void WaClient::run_until(const std::function<bool()>& stop, int timeout_seconds) {
auto clock_ms = []() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
};
keepalive_.start(clock_ms());
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_seconds);
while (std::chrono::steady_clock::now() < deadline) {
if (stop && stop()) return;
// a closed stream returns from recv at once; without this check the loop spun at 100% CPU until the deadline
if (!socket_open()) return;
if (keepalive_.due(clock_ms())) {
if (send_keepalive_ping()) {
keepalive_.record_pong(clock_ms());
} else if (keepalive_.record_timeout(clock_ms())) {
log("keepalive failed for " + std::to_string(WaConsts::kWaKeepAliveMaxFailTime) +
"s, disconnecting (whatsmeow keepAliveLoop force-reconnect; the redial lands with M9 auto-reconnect)");
disconnect();
return;
}
}
auto node = recv_node(2);
if (node) dispatch(*node);
}
@@ -428,15 +453,53 @@ std::optional<WaNode> WaClient::send_iq(const WaNode& iq, int timeout_seconds) {
send_node(iq);
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_seconds);
while (std::chrono::steady_clock::now() < deadline) {
if (!socket_open()) return std::nullopt;
if (!socket_open()) {
log("iq " + id + ": " + WaDisconnectedError("info query").message());
return std::nullopt;
}
auto node = recv_node(2);
if (!node) continue;
if (node->tag == "iq" && node->attr("id") == id) return node;
if (WaIq::is_disconnect_node(*node)) {
std::string why = WaDisconnectedError("info query", *node).message();
why += WaIq::is_auth_error_disconnect(*node) ? ", not retrying (auth error)"
: ", retry needs M9 auto-reconnect";
log("iq " + id + ": " + why);
return std::nullopt;
}
if (node->tag == "iq" && node->attr("id") == id) {
if (node->attr("type") == "error") log("iq " + id + " error: " + WaIq::parse_error(*node).message());
return node;
}
dispatch(*node); // handle anything else (keepalive, notifications)
}
log("iq " + id + ": " + WaIqError::timed_out().message());
return std::nullopt;
}
std::optional<WaNode> WaClient::query_iq(WaIqQuery q) {
if (q.id.empty()) q.id = next_iq_id();
const int wait = q.timeout_seconds != 0 ? q.timeout_seconds : WaConsts::kWaIqDefaultTimeout;
return send_iq(WaIq::build(q), wait);
}
bool WaClient::send_keepalive_ping() {
WaIqQuery q;
q.ns = "w:p";
q.type = "get";
q.to = WaConsts::kServerJid;
q.timeout_seconds = WaConsts::kWaKeepAliveResponseDeadline;
try {
auto reply = query_iq(q);
// any reply proves liveness, even an error one (whatsmeow sendKeepAlive returns success on any response)
const bool answered = reply.has_value();
if (!answered) log("keepalive ping unanswered");
return answered;
} catch (const std::exception& e) {
log(std::string("keepalive ping failed: ") + e.what());
return false;
}
}
std::vector<std::string> WaClient::usync_devices(const std::vector<std::string>& jids) {
WaNode query("query");
WaNode devices("devices");
@@ -1087,10 +1150,12 @@ void WaClient::sync_app_state(int key_wait_seconds) {
}
void WaClient::disconnect() {
const bool was_open = socket_open();
if (sock_) sock_->close();
sock_.reset();
noise_.reset();
logged_in_ = false;
if (was_open && on_disconnect) on_disconnect();
}
} // namespace detz::wa
+14 -2
View File
@@ -11,7 +11,10 @@
#include <string>
#include <vector>
#include "wa_consts.h"
#include "wa_device.h"
#include "wa_iq.h"
#include "wa_keepalive.h"
#include "wa_node.h"
#include "wa_noise.h"
#include "wa_socket.h"
@@ -68,6 +71,7 @@ class WaClient {
std::function<void(const std::vector<std::string>& qr_codes)> on_qr; // registration: QR strings to show
std::function<void(const std::string& jid)> on_paired; // pairing completed, device saved
std::function<void(const std::string& text)> on_log; // human-readable progress
std::function<void()> on_disconnect; // the stream closed (socket.DisconnectHandler role)
// Connect, run the Noise handshake, and send the ClientPayload (registration
// when the device is unpaired, login when paired). Returns false on failure.
@@ -148,6 +152,8 @@ class WaClient {
bool restart_seen_{false}; // stream:error 515 received
int failure_code_{0}; // reason from the last <failure> node
uint64_t iq_counter_{0};
std::string iq_prefix_; // "<byte0>.<byte1>-" request-id prefix, drawn once per client
WaKeepAlive keepalive_; // ping schedule for run_until (whatsmeow keepAliveLoop role)
Bytes last_plaintext_; // the most recent decrypted frame, for capture
std::string capture_path_; // where to record a frame that broke a parser
@@ -184,8 +190,14 @@ class WaClient {
std::string next_iq_id();
// --- send path helpers (live, unverified in-tree) ---
// one synchronous iq request/response, matching by id
std::optional<WaNode> send_iq(const WaNode& iq, int timeout_seconds);
// one synchronous iq request/response, matching by id; error replies are
// logged with their code/text and returned for the caller, disconnects and
// timeouts log loudly and yield nothing
std::optional<WaNode> send_iq(const WaNode& iq, int timeout_seconds = WaConsts::kWaIqDefaultTimeout);
// the same through a WaIqQuery: fills the id when empty, builds the stanza
std::optional<WaNode> query_iq(WaIqQuery q);
// one keepalive ping (iq get xmlns w:p); any reply proves liveness
bool send_keepalive_ping();
// device JIDs for a set of user JIDs via usync
std::vector<std::string> usync_devices(const std::vector<std::string>& jids);
// ensure a Signal session exists for each device jid (fetch prekeys if not)
+14
View File
@@ -48,6 +48,20 @@ struct WaConsts {
static constexpr const char* kWaReceiptRetry = "retry";
// vCard contact import fallback (--whatsapp-import)
static constexpr size_t kWaVcfFileMax = 8u * 1024 * 1024; // refuse a bigger .vcf, loud
// Noise server certificate (whatsmeow handshake.go WACertPubKey/WACertIssuerSerial): the pinned key the
// server's intermediate cert must be signed with, and the intermediate's required issuer serial.
static constexpr std::array<uint8_t, 32> kWaCertPubKey = {0x14, 0x23, 0x75, 0x57, 0x4d, 0x0a, 0x58, 0x71,
0x66, 0xaa, 0xe7, 0x1e, 0xbe, 0x51, 0x64, 0x37,
0xc4, 0xa2, 0x8b, 0x73, 0xe3, 0x69, 0x5c, 0x6c,
0xe1, 0xf7, 0xf9, 0x54, 0x5d, 0xa8, 0xee, 0x6b};
static constexpr uint32_t kWaCertIssuerSerial = 0;
// iq request/response (whatsmeow request.go defaultRequestTimeout): every synchronous iq wait ends here.
static constexpr int kWaIqDefaultTimeout = 75; // seconds
// socket keepalive (whatsmeow keepalive.go): jittered iq ping, reply deadline, force-reconnect horizon.
static constexpr int kWaKeepAliveResponseDeadline = 10; // seconds to wait for one ping reply
static constexpr int kWaKeepAliveIntervalMin = 20; // seconds, jitter floor between pings
static constexpr int kWaKeepAliveIntervalMax = 30; // seconds, jitter ceiling between pings
static constexpr int kWaKeepAliveMaxFailTime = 180; // seconds of failed pings before a forced reconnect
};
} // namespace detz::wa
+91
View File
@@ -0,0 +1,91 @@
#include "wa_iq.h"
#include <charconv>
#include <string_view>
#include <system_error>
#include "wa_crypto.h"
namespace detz::wa {
WaIqError::WaIqError(int code, std::string text) : code_(code), text_(std::move(text)) {}
WaIqError::WaIqError(int code, std::string text, std::optional<WaNode> error_node, std::optional<WaNode> raw)
: code_(code), text_(std::move(text)), error_node_(std::move(error_node)), raw_(std::move(raw)) {}
WaIqError WaIqError::timed_out() { return WaIqError(-1, "timed out"); }
std::string WaIqError::message() const {
if (code_ < 0) return "info query " + text_;
if (code_ == 0) {
if (error_node_.has_value()) return "info query returned unknown error: " + error_node_->debug();
if (raw_.has_value()) return "info query returned unexpected response: " + raw_->debug();
return "unknown info query error";
}
return "info query returned status " + std::to_string(code_) + ": " + text_;
}
bool WaIqError::is(const WaIqError& other) const {
if (code_ != 0 && other.code_ != 0) return code_ == other.code_ && text_ == other.text_;
const bool same_node = error_node_.has_value() && other.error_node_.has_value() &&
error_node_->debug() == other.error_node_->debug();
return same_node;
}
WaDisconnectedError::WaDisconnectedError(std::string action, std::optional<WaNode> node)
: action_(std::move(action)), node_(std::move(node)) {}
std::string WaDisconnectedError::message() const {
return "websocket disconnected before " + action_ + " returned response";
}
WaNode WaIq::build(const WaIqQuery& q) {
WaNode iq("iq");
iq.attrs["id"] = q.id;
iq.attrs["xmlns"] = q.ns;
iq.attrs["type"] = q.type;
if (!q.smax_id.empty()) iq.attrs["smax_id"] = q.smax_id;
if (!q.to.empty()) iq.attrs["to"] = q.to;
if (!q.target.empty()) iq.attrs["target"] = q.target;
if (q.content.has_value()) iq.content = WaNode::Children{*q.content};
return iq;
}
WaIqError WaIq::parse_error(const WaNode& node) {
WaIqError none(0, "", std::nullopt, node);
const WaNode* e = node.child("error");
if (e == nullptr) return none;
// whatsmeow OptionalInt: the attribute is a string on the wire; absent or
// unparseable reads as 0. from_chars never throws and cannot overflow.
int code = 0;
const std::string raw = e->attr("code");
if (!raw.empty()) {
int parsed = 0;
const std::string_view view(raw);
auto res = std::from_chars(view.data(), view.data() + view.size(), parsed);
if (res.ec == std::errc() && res.ptr == view.data() + view.size()) code = parsed;
}
return WaIqError(code, e->attr("text"), *e, node);
}
bool WaIq::is_disconnect_node(const WaNode& node) {
return node.tag == "xmlstreamend" || node.tag == "stream:error";
}
bool WaIq::is_auth_error_disconnect(const WaNode& node) {
const WaNode* conflict = node.tag == "stream:error" ? node.child("conflict") : nullptr;
const std::string ctype = conflict != nullptr ? conflict->attr("type") : std::string();
return node.tag == "stream:error" &&
(node.attr("code") == "401" || ctype == "replaced" || ctype == "device_removed");
}
std::string WaIq::generate_id(const std::string& prefix, uint64_t counter) {
return prefix + std::to_string(counter);
}
std::string WaIq::new_prefix() {
Bytes r = WaCrypto::random_bytes(2);
return std::to_string(r[0]) + "." + std::to_string(r[1]) + "-";
}
} // namespace detz::wa
+105
View File
@@ -0,0 +1,105 @@
#pragma once
// WaIq: info-query (iq) request/response infrastructure. Builds iq stanzas,
// parses iq error replies into the typed whatsmeow taxonomy, and recognizes
// stream disconnects, following whatsmeow request.go and errors.go (MPL-2.0).
// Synchronous single-flight use: the caller sends the built node and matches
// the reply by id (see WaClient::send_iq). See docs/whatsapp.md section 18.
#include <cstdint>
#include <optional>
#include <string>
#include "wa_node.h"
namespace detz::wa {
// One info query (whatsmeow infoQuery): namespace, get/set, addressing, the
// reply-matching id, and the wait deadline. infoQuery.NoRetry has no member
// here: reconnect-triggered retries arrive with M9 auto-reconnect, and there
// is nothing to retry against today.
struct WaIqQuery {
std::string ns; // xmlns
std::string type; // "get" or "set"
std::string to; // recipient JID, empty when the server is implied
std::string target; // target JID for user-scoped queries, usually empty
std::string id; // reply-matching id; the send path fills it when empty
std::string smax_id; // smax_id attribute, empty when unused
std::optional<WaNode> content; // single child node, if the query carries one
int timeout_seconds{0}; // wait deadline, 0 selects kWaIqDefaultTimeout
};
// Typed iq failure (whatsmeow IQError in errors.go): the numeric code and text
// from the <error> child, plus the nodes for debugging. A value, not an
// exception: the send path logs it and returns the reply for the caller.
class WaIqError {
public:
WaIqError(int code, std::string text);
WaIqError(int code, std::string text, std::optional<WaNode> error_node, std::optional<WaNode> raw);
int code() const { return code_; }
const std::string& text() const { return text_; }
bool has_error_node() const { return error_node_.has_value(); }
// Human rendering (whatsmeow IQError.Error).
std::string message() const;
// Equivalence (whatsmeow IQError.Is): same code+text, else identical nodes.
bool is(const WaIqError& other) const;
// Local send failures with no server reply.
static WaIqError timed_out(); // whatsmeow ErrIQTimedOut
// Common server replies (whatsmeow ErrIQ* in errors.go), for errors.Is-style use.
static WaIqError bad_request() { return WaIqError(400, "bad-request"); }
static WaIqError not_authorized() { return WaIqError(401, "not-authorized"); }
static WaIqError forbidden() { return WaIqError(403, "forbidden"); }
static WaIqError not_found() { return WaIqError(404, "item-not-found"); }
static WaIqError not_allowed() { return WaIqError(405, "not-allowed"); }
static WaIqError not_acceptable() { return WaIqError(406, "not-acceptable"); }
static WaIqError gone() { return WaIqError(410, "gone"); }
static WaIqError resource_limit() { return WaIqError(419, "resource-limit"); }
static WaIqError locked() { return WaIqError(423, "locked"); }
static WaIqError rate_over_limit() { return WaIqError(429, "rate-overlimit"); }
static WaIqError internal_server_error() { return WaIqError(500, "internal-server-error"); }
static WaIqError service_unavailable() { return WaIqError(503, "service-unavailable"); }
static WaIqError partial_server_error() { return WaIqError(530, "partial-server-error"); }
private:
int code_{0};
std::string text_;
std::optional<WaNode> error_node_;
std::optional<WaNode> raw_;
};
// The stream died before a reply arrived (whatsmeow DisconnectedError).
class WaDisconnectedError {
public:
explicit WaDisconnectedError(std::string action, std::optional<WaNode> node = std::nullopt);
static WaDisconnectedError for_iq() { return WaDisconnectedError("info query"); } // ErrIQDisconnected
const std::string& action() const { return action_; }
// Human rendering (whatsmeow DisconnectedError.Error).
std::string message() const;
// Equivalence (whatsmeow DisconnectedError.Is): same action.
bool is(const WaDisconnectedError& other) const { return action_ == other.action_; }
private:
std::string action_;
std::optional<WaNode> node_;
};
class WaIq {
public:
// Assemble the iq stanza (whatsmeow sendIQAsyncAndGetData attributes).
static WaNode build(const WaIqQuery& q);
// Classify a reply (whatsmeow parseIQError): reads the <error> child when
// present, else yields code 0 ("unexpected response"). Never fails.
static WaIqError parse_error(const WaNode& node);
// A node that ends the stream (whatsmeow isDisconnectNode).
static bool is_disconnect_node(const WaNode& node);
// A disconnect no reconnect would survive: 401, replaced, device_removed
// (whatsmeow isAuthErrorDisconnect).
static bool is_auth_error_disconnect(const WaNode& node);
// Request ids (whatsmeow generateRequestID/NewClient uniqueID): a per-client
// "<byte0>.<byte1>-" prefix plus a counter.
static std::string generate_id(const std::string& prefix, uint64_t counter);
static std::string new_prefix();
};
} // namespace detz::wa
+34
View File
@@ -0,0 +1,34 @@
#include "wa_keepalive.h"
#include <random>
namespace detz::wa {
WaKeepAlive::WaKeepAlive(IntervalFn next) : next_(std::move(next)) {}
WaKeepAlive::Ms WaKeepAlive::default_interval() {
static thread_local std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<Ms> pick(WaConsts::kWaKeepAliveIntervalMin * 1000,
WaConsts::kWaKeepAliveIntervalMax * 1000);
return pick(rng);
}
void WaKeepAlive::start(Ms now_ms) {
started_ = true;
last_success_ms_ = now_ms;
next_due_ms_ = now_ms + next_();
}
void WaKeepAlive::record_pong(Ms now_ms) {
errors_ = 0;
last_success_ms_ = now_ms;
next_due_ms_ = now_ms + next_();
}
bool WaKeepAlive::record_timeout(Ms now_ms) {
errors_ += 1;
next_due_ms_ = now_ms + next_();
return now_ms - last_success_ms_ > WaConsts::kWaKeepAliveMaxFailTime * 1000;
}
} // namespace detz::wa
+43
View File
@@ -0,0 +1,43 @@
#pragma once
// WaKeepAlive: the websocket keepalive schedule (whatsmeow keepalive.go). A
// ping is due every 20-30 s (jittered); each unanswered ping counts an error,
// and failures spanning kWaKeepAliveMaxFailTime force a reconnect. Pure
// timing state over an injected millisecond clock, so tests drive it
// deterministically; WaClient sends the actual w:p iq. See docs/whatsapp.md
// section 18.
#include <cstdint>
#include <functional>
#include "wa_consts.h"
namespace detz::wa {
class WaKeepAlive {
public:
using Ms = int64_t;
using IntervalFn = std::function<Ms()>; // wait until the next ping
explicit WaKeepAlive(IntervalFn next = default_interval);
void start(Ms now_ms);
bool due(Ms now_ms) const { return started_ && now_ms >= next_due_ms_; }
Ms next_due_ms() const { return next_due_ms_; }
// A ping reply arrived: errors clear, the schedule advances from now.
void record_pong(Ms now_ms);
// A ping went unanswered. True when the client must reconnect: failures
// spanning more than kWaKeepAliveMaxFailTime (whatsmeow KeepAliveMaxFailTime).
bool record_timeout(Ms now_ms);
int error_count() const { return errors_; }
Ms last_success_ms() const { return last_success_ms_; }
private:
static Ms default_interval(); // uniform 20-30 s, like whatsmeow keepAliveLoop
IntervalFn next_;
Ms next_due_ms_{0};
Ms last_success_ms_{0};
int errors_{0};
bool started_{false};
};
} // namespace detz::wa
+18 -10
View File
@@ -17,11 +17,17 @@ Bytes WaFraming::frame(const Bytes& payload, bool with_header) {
}
std::optional<Bytes> WaFraming::unframe(const Bytes& buf, size_t& pos) {
if (pos + 3 > buf.size()) return std::nullopt;
// Routine negative, deliberately silent: "no complete frame yet" fires on
// every partial read and the caller keeps pumping; it is not a failure.
const std::optional<Bytes> kIncomplete;
// Overflow-safe subtraction checks (docs/cpp-safety.md Layer 1): pos is
// caller-owned, so establish pos <= size first, then compare lengths.
if (pos > buf.size()) return kIncomplete;
if (kLengthSize > buf.size() - pos) return kIncomplete;
uint32_t n = (static_cast<uint32_t>(buf[pos]) << 16) | (static_cast<uint32_t>(buf[pos + 1]) << 8) | buf[pos + 2];
if (pos + 3 + n > buf.size()) return std::nullopt;
Bytes out(buf.begin() + pos + 3, buf.begin() + pos + 3 + n);
pos += 3 + n;
if (n > buf.size() - pos - kLengthSize) return kIncomplete;
Bytes out(buf.begin() + pos + kLengthSize, buf.begin() + pos + kLengthSize + n);
pos += kLengthSize + n;
return out;
}
@@ -51,6 +57,11 @@ void WaNoise::mix_key(const Bytes& data) {
counter_ = 0;
}
void WaNoise::mix_shared_secret(const std::array<uint8_t, 32>& priv, const std::array<uint8_t, 32>& pub) {
auto secret = WaCrypto::x25519(priv, pub);
mix_key(Bytes(secret.begin(), secret.end()));
}
Bytes WaNoise::iv(uint32_t counter) {
Bytes v(12, 0);
v[8] = (counter >> 24) & 0xff;
@@ -100,24 +111,21 @@ std::optional<WaNoise::HandshakeResult> WaNoise::client_handshake(const std::arr
mix_hash(sh->ephemeral);
std::array<uint8_t, 32> se_arr{};
std::copy(sh->ephemeral.begin(), sh->ephemeral.end(), se_arr.begin());
auto es = WaCrypto::x25519(eph_priv, se_arr);
mix_key(Bytes(es.begin(), es.end()));
mix_shared_secret(eph_priv, se_arr);
auto server_static = decrypt(sh->static_enc);
if (!server_static || server_static->size() != 32) return std::nullopt;
res.server_static = *server_static;
std::array<uint8_t, 32> ss_pub{};
std::copy(server_static->begin(), server_static->end(), ss_pub.begin());
auto ss = WaCrypto::x25519(eph_priv, ss_pub);
mix_key(Bytes(ss.begin(), ss.end()));
mix_shared_secret(eph_priv, ss_pub);
auto server_payload = decrypt(sh->payload);
if (!server_payload) return std::nullopt;
res.server_payload = *server_payload;
Bytes static_enc = encrypt(Bytes(static_pub.begin(), static_pub.end()));
auto se2 = WaCrypto::x25519(static_priv, se_arr);
mix_key(Bytes(se2.begin(), se2.end()));
mix_shared_secret(static_priv, se_arr);
Bytes payload_enc = encrypt(client_payload);
res.client_finish = encode_client_finish(static_enc, payload_enc);
+4
View File
@@ -19,6 +19,7 @@ class WaFraming {
public:
static constexpr std::array<uint8_t, 4> kHeader = {0x57, 0x41, 0x06, 0x03};
static constexpr size_t kMaxFrame = 1u << 24;
static constexpr size_t kLengthSize = 3; // socket.FrameLengthSize: the big-endian length prefix
// Wrap a payload in a length-prefixed frame; with_header prepends the WA magic.
static Bytes frame(const Bytes& payload, bool with_header);
// Read one length-prefixed frame from buf starting at pos; advances pos.
@@ -47,6 +48,9 @@ class WaNoise {
const std::array<uint8_t, 32>& static_pub,
const Bytes& server_hello_msg, const Bytes& client_payload);
// Mix an X25519 shared secret into the handshake key (socket.NoiseHandshake.MixSharedSecretIntoKey).
void mix_shared_secret(const std::array<uint8_t, 32>& priv, const std::array<uint8_t, 32>& pub);
// transport mode (after handshake): encrypt/decrypt a frame payload.
Bytes transport_encrypt(const Bytes& plaintext);
std::optional<Bytes> transport_decrypt(const Bytes& ciphertext);
+15 -8
View File
@@ -307,17 +307,17 @@ std::optional<WaCertChainParsed> parse_cert_chain(const Bytes& b) {
ProtoReader::Field f;
while (r.next(f)) {
if (f.wire != 2) continue;
// CertChain: intermediate = 1, leaf = 2 (each a NoiseCertificate)
// CertChain (WACert.pb.go): leaf = 1, intermediate = 2 (each a NoiseCertificate).
if (f.field == 1) {
if (auto nc = parse_noise_certificate(f.bytes)) {
o.intermediate_details = nc->details;
o.intermediate_signature = nc->signature;
}
} else if (f.field == 2) {
if (auto nc = parse_noise_certificate(f.bytes)) {
o.leaf_details = nc->details;
o.leaf_signature = nc->signature;
}
} else if (f.field == 2) {
if (auto nc = parse_noise_certificate(f.bytes)) {
o.intermediate_details = nc->details;
o.intermediate_signature = nc->signature;
}
}
}
return o;
@@ -339,9 +339,16 @@ std::optional<WaNoiseCertDetails> parse_cert_details(const Bytes& b) {
WaNoiseCertDetails o;
ProtoReader r(b);
ProtoReader::Field f;
// Details (WACert.pb.go): serial = 1, issuer_serial = 2, key = 3, not_before = 4, not_after = 5.
while (r.next(f)) {
if (f.field == 2 && f.wire == 0) o.serial = static_cast<uint32_t>(f.varint);
else if (f.field == 3 && f.wire == 2) o.key = f.bytes; // key field in NoiseCertificate.Details
if (f.wire == 0) {
if (f.field == 1) o.serial = static_cast<uint32_t>(f.varint);
else if (f.field == 2) o.issuer_serial = static_cast<uint32_t>(f.varint);
else if (f.field == 4) o.not_before = f.varint;
else if (f.field == 5) o.not_after = f.varint;
} else if (f.field == 3 && f.wire == 2) {
o.key = f.bytes;
}
}
return o;
}
+6 -1
View File
@@ -138,9 +138,14 @@ struct WaADVDeviceIdentity {
std::optional<WaADVDeviceIdentity> parse_adv_device_identity(const Bytes& b);
// ---- cert chain (noise handshake) ----
// waCert.CertChain_NoiseCertificate.Details (fields 1..5): serial, issuer
// serial, subject key, validity window. Verified by WaServerCert.
struct WaNoiseCertDetails {
uint32_t serial{0};
Bytes key; // the issuer/subject key (32 bytes)
uint32_t issuer_serial{0};
Bytes key; // the subject key (32 bytes)
uint64_t not_before{0};
uint64_t not_after{0};
};
struct WaCertChainParsed {
Bytes intermediate_details, intermediate_signature;
+9 -1
View File
@@ -24,6 +24,7 @@ void WaSocket::close() {
}
void WaSocket::connect(int timeout_seconds) {
if (connected_) throw WaSocketError("connect on an already open socket");
CURL* c = static_cast<CURL*>(handle_);
curl_easy_setopt(c, CURLOPT_URL, kUrl);
curl_easy_setopt(c, CURLOPT_CONNECT_ONLY, 2L); // 2L = WebSocket connect-only mode
@@ -33,12 +34,19 @@ void WaSocket::connect(int timeout_seconds) {
curl_easy_setopt(c, CURLOPT_TIMEOUT, static_cast<long>(timeout_seconds));
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, static_cast<long>(timeout_seconds));
CURLcode rc = curl_easy_perform(c);
long http_status = 0;
if (rc != CURLE_OK) curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &http_status);
curl_slist_free_all(hdrs);
if (rc != CURLE_OK) throw WaSocketError(std::string("connect: ") + curl_easy_strerror(rc));
if (rc != CURLE_OK) {
if (http_status != 0)
throw WaSocketStatusError(std::string("connect: ") + curl_easy_strerror(rc), http_status);
throw WaSocketError(std::string("connect: ") + curl_easy_strerror(rc));
}
connected_ = true;
}
void WaSocket::send_frame(const Bytes& framed) {
if (!connected_ || handle_ == nullptr) throw WaSocketError("send on a socket that is not connected");
CURL* c = static_cast<CURL*>(handle_);
size_t off = 0;
while (off < framed.size()) {
+12
View File
@@ -18,6 +18,18 @@ class WaSocketError : public std::runtime_error {
explicit WaSocketError(const std::string& m) : std::runtime_error("wa socket: " + m) {}
};
// A connect failure where the server answered HTTP (socket.ErrWithStatusCode):
// the status code travels with the error instead of hiding in the text.
class WaSocketStatusError : public WaSocketError {
public:
WaSocketStatusError(const std::string& m, long status)
: WaSocketError(m + " (http " + std::to_string(status) + ")"), status_(status) {}
long status() const { return status_; }
private:
long status_{0};
};
class WaSocket {
public:
static constexpr const char* kUrl = "wss://web.whatsapp.com/ws/chat";
+160
View File
@@ -0,0 +1,160 @@
// Oracle generator for detz::wa Noise handshake + server-cert vectors (M1).
//
// Runs the Noise_XX handshake over the wa-go noise.json trace with whatsmeow's
// OWN socket.NoiseHandshake (pinned version) and checks the decrypted server
// certificate with whatsmeow's waCert protobufs plus libsignal ed25519, step
// by step as the unexported verifyServerCert does. The C++ WaNoise handshake
// bytes, WaNoiseCertDetails field numbers and WaServerCert verdicts are
// checked against this output, not against themselves.
//
// Usage: go run cert_oracle.go noise.json > cert_vectors.json
// (inside a module that requires go.mau.fi/whatsmeow)
//go:build ignore
package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"time"
"google.golang.org/protobuf/proto"
"go.mau.fi/libsignal/ecc"
whatsmeow "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waCert"
"go.mau.fi/whatsmeow/proto/waWa6"
"go.mau.fi/whatsmeow/socket"
)
func mustHex(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
fmt.Fprintf(os.Stderr, "hex: %v\n", err)
os.Exit(1)
}
return b
}
func b32(b []byte) [32]byte {
var a [32]byte
copy(a[:], b)
return a
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: cert_oracle.go noise.json")
os.Exit(1)
}
raw, err := os.ReadFile(os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
var vec map[string]string
if err := json.Unmarshal(raw, &vec); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
ephPriv, ephPub := mustHex(vec["ephemeralKeyPriv"]), mustHex(vec["ephemeralKeyPub"])
header := mustHex(vec["noiseHeader"])
frame := mustHex(vec["serverHelloFrameHex"])
// Server frames carry a 3-byte big-endian length prefix, no WA magic.
if len(frame) < 3 {
fmt.Fprintln(os.Stderr, "server hello frame too short")
os.Exit(1)
}
helloBytes := frame[3:]
// --- handshake through the certificate, exactly as doHandshake ---
nh := socket.NewNoiseHandshake()
nh.Start(socket.NoiseStartPattern, header)
nh.Authenticate(ephPub)
var hello waWa6.HandshakeMessage
if err := proto.Unmarshal(helloBytes, &hello); err != nil {
fmt.Fprintf(os.Stderr, "unmarshal hello: %v\n", err)
os.Exit(1)
}
serverEph := hello.GetServerHello().GetEphemeral()
serverStaticCT := hello.GetServerHello().GetStatic()
certCT := hello.GetServerHello().GetPayload()
nh.Authenticate(serverEph)
if err := nh.MixSharedSecretIntoKey(b32(ephPriv), b32(serverEph)); err != nil {
fmt.Fprintf(os.Stderr, "mix server ephemeral: %v\n", err)
os.Exit(1)
}
staticDec, err := nh.Decrypt(serverStaticCT)
if err != nil {
fmt.Fprintf(os.Stderr, "decrypt server static: %v\n", err)
os.Exit(1)
}
if err := nh.MixSharedSecretIntoKey(b32(ephPriv), b32(staticDec)); err != nil {
fmt.Fprintf(os.Stderr, "mix server static: %v\n", err)
os.Exit(1)
}
certDec, err := nh.Decrypt(certCT)
if err != nil {
fmt.Fprintf(os.Stderr, "decrypt cert: %v\n", err)
os.Exit(1)
}
// --- verifyServerCert, check by check (handshake.go) ---
checks := map[string]bool{}
var chain waCert.CertChain
checks["chain_unmarshal"] = proto.Unmarshal(certDec, &chain) == nil
imDetailsRaw := chain.GetIntermediate().GetDetails()
imSig := chain.GetIntermediate().GetSignature()
leafDetailsRaw := chain.GetLeaf().GetDetails()
leafSig := chain.GetLeaf().GetSignature()
checks["parts_present"] = imDetailsRaw != nil && imSig != nil && leafDetailsRaw != nil && leafSig != nil
checks["intermediate_sig_len_64"] = len(imSig) == 64
checks["leaf_sig_len_64"] = len(leafSig) == 64
checks["intermediate_sig_valid"] = checks["intermediate_sig_len_64"] &&
ecc.VerifySignature(ecc.NewDjbECPublicKey(whatsmeow.WACertPubKey), imDetailsRaw, [64]byte(imSig))
var imDetails waCert.CertChain_NoiseCertificate_Details
checks["intermediate_details_unmarshal"] = proto.Unmarshal(imDetailsRaw, &imDetails) == nil
checks["intermediate_issuer_is_zero"] = imDetails.GetIssuerSerial() == whatsmeow.WACertIssuerSerial
checks["intermediate_key_len_32"] = len(imDetails.GetKey()) == 32
checks["leaf_sig_valid"] = checks["leaf_sig_len_64"] && checks["intermediate_key_len_32"] &&
ecc.VerifySignature(ecc.NewDjbECPublicKey([32]byte(imDetails.GetKey())), leafDetailsRaw, [64]byte(leafSig))
now := time.Now().Unix()
checks["intermediate_valid_now"] = now >= int64(imDetails.GetNotBefore()) && now <= int64(imDetails.GetNotAfter())
var leafDetails waCert.CertChain_NoiseCertificate_Details
checks["leaf_details_unmarshal"] = proto.Unmarshal(leafDetailsRaw, &leafDetails) == nil
checks["leaf_issuer_matches"] = leafDetails.GetIssuerSerial() == imDetails.GetSerial()
checks["leaf_key_matches_static"] = bytes.Equal(leafDetails.GetKey(), staticDec)
checks["leaf_valid_now"] = now >= int64(leafDetails.GetNotBefore()) && now <= int64(leafDetails.GetNotAfter())
overall := true
for _, v := range checks {
overall = overall && v
}
out := map[string]any{
"server_static_hex": hex.EncodeToString(staticDec),
"server_payload_hex": hex.EncodeToString(certDec),
"now_unix": now,
"checks": checks,
"overall_now": overall,
"wacert_pubkey_hex": hex.EncodeToString(whatsmeow.WACertPubKey[:]),
"intermediate": map[string]any{
"serial": imDetails.GetSerial(), "issuer_serial": imDetails.GetIssuerSerial(),
"key_hex": hex.EncodeToString(imDetails.GetKey()),
"not_before": imDetails.GetNotBefore(), "not_after": imDetails.GetNotAfter(),
},
"leaf": map[string]any{
"serial": leafDetails.GetSerial(), "issuer_serial": leafDetails.GetIssuerSerial(),
"key_hex": hex.EncodeToString(leafDetails.GetKey()),
"not_before": leafDetails.GetNotBefore(), "not_after": leafDetails.GetNotAfter(),
},
}
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
if err := enc.Encode(out); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
@@ -0,0 +1 @@
{"checks":{"chain_unmarshal":true,"intermediate_details_unmarshal":true,"intermediate_issuer_is_zero":true,"intermediate_key_len_32":true,"intermediate_sig_len_64":true,"intermediate_sig_valid":true,"intermediate_valid_now":true,"leaf_details_unmarshal":true,"leaf_issuer_matches":true,"leaf_key_matches_static":true,"leaf_sig_len_64":true,"leaf_sig_valid":true,"leaf_valid_now":false,"parts_present":true},"intermediate":{"issuer_serial":0,"key_hex":"1c51a9ac303994c6c8d0b92ea1878a533476599cc599fbea35997d9aa90cce62","not_after":1804463999,"not_before":1741392000,"serial":3},"leaf":{"issuer_serial":3,"key_hex":"91e0d75970c27845af977f8bcebcd453e2b94877ac3af0ecc59bf0de0da77f70","not_after":1783976400,"not_before":1773090000,"serial":354},"now_unix":1793153919,"overall_now":false,"server_payload_hex":"0a770a3308e20210031a2091e0d75970c27845af977f8bcebcd453e2b94877ac3af0ecc59bf0de0da77f7020d0e9bccd0628d0a3d5d20612404a2294f6192dff80193518756feb5a4688dd4d43404b7955579e15170f646963343e35f7ba1ea5a0697ce86f175d0575c2809edc31854f3fc5af4eeee124a80a12760a32080310001a201c51a9ac303994c6c8d0b92ea1878a533476599cc599fbea35997d9aa90cce62208091aebe0628ffdeb7dc061240270f294648539fed4870e25054dd4e95983aba29189c2ba6c8eeda7055555f753740f5ec192ab64c26c26d6ade6d20b9f774aee37120a6b20395f53c66058507","server_static_hex":"91e0d75970c27845af977f8bcebcd453e2b94877ac3af0ecc59bf0de0da77f70","wacert_pubkey_hex":"142375574d0a587166aae71ebe516437c4a28b73e3695c6ce1f7f9545da8ee6b"}
+203
View File
@@ -0,0 +1,203 @@
// Oracle generator for detz::wa iq request/response vectors (M1).
//
// whatsmeow's own binary.Marshal produces the wire bytes and whatsmeow's own
// AttrGetter extracts error code/text exactly as the unexported parseIQError
// does, so the C++ WaIq decoder/classifier is checked against the reference.
// whatsmeow writes attributes in Go map order (random), so multi-attribute
// nodes are decode-direction only; nodes with <= 2 attributes also serve the
// encode direction (single wire_hex when deterministic, wire_set otherwise).
//
// Run from a module that requires go.mau.fi/whatsmeow:
// go run iq_oracle.go > iq_vectors.json (inside a module that requires go.mau.fi/whatsmeow)
//go:build ignore
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
"sort"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
)
func mustMarshal(n waBinary.Node) string {
raw, err := waBinary.Marshal(n)
if err != nil {
fmt.Fprintf(os.Stderr, "marshal: %v\n", err)
os.Exit(1)
}
return hex.EncodeToString(raw)
}
// marshalSet remarshals until the order distribution stabilizes (or 200
// tries): whatsmeow's attribute order is random, so the C++ encoder's fixed
// order must be a member of the observed set.
func marshalSet(n waBinary.Node) []string {
seen := map[string]bool{}
var out []string
for i := 0; i < 200; i++ {
h := mustMarshal(n)
if !seen[h] {
seen[h] = true
out = append(out, h)
}
}
sort.Strings(out)
return out
}
// errorOf mirrors whatsmeow parseIQError (errors.go) using only exported calls.
func errorOf(n waBinary.Node) (int, string) {
val, ok := n.GetOptionalChildByTag("error")
if !ok {
return 0, ""
}
ag := val.AttrGetter()
return ag.OptionalInt("code"), ag.OptionalString("text")
}
type decodeVec struct {
Name string `json:"name"`
WireHex string `json:"wire_hex"`
Tag string `json:"tag"`
Attrs map[string]any `json:"attrs"`
Children []string `json:"children"`
Code int `json:"code"`
Text string `json:"text"`
HasError bool `json:"has_error"`
}
type encodeVec struct {
Name string `json:"name"`
Tag string `json:"tag"`
Attrs map[string]any `json:"attrs"`
WireHex string `json:"wire_hex,omitempty"`
WireSet []string `json:"wire_set,omitempty"`
}
func strmap(a waBinary.Attrs) map[string]any {
m := map[string]any{}
for k, v := range a {
m[k] = fmt.Sprint(v)
}
return m
}
func main() {
id := "42.7-9"
var dec []decodeVec
add := func(name string, n waBinary.Node, children []string) {
wire := mustMarshal(n)
// Extract from the unmarshaled node, as the real receive path does:
// on the wire every attribute is a string.
raw, err := hex.DecodeString(wire)
if err != nil {
fmt.Fprintf(os.Stderr, "hex: %v\n", err)
os.Exit(1)
}
unpacked, err := waBinary.Unpack(raw)
if err != nil {
fmt.Fprintf(os.Stderr, "unpack %s: %v\n", name, err)
os.Exit(1)
}
back, err := waBinary.Unmarshal(unpacked)
if err != nil {
fmt.Fprintf(os.Stderr, "unmarshal %s: %v\n", name, err)
os.Exit(1)
}
code, text := errorOf(*back)
_, hasErr := back.GetOptionalChildByTag("error")
dec = append(dec, decodeVec{
Name: name, WireHex: wire, Tag: back.Tag,
Attrs: strmap(back.Attrs), Children: children,
Code: code, Text: text, HasError: hasErr,
})
}
// 1. keepalive ping (whatsmeow keepalive.go sendKeepAlive shape).
add("keepalive_ping", waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{"id": id, "xmlns": "w:p", "type": "get", "to": types.ServerJID},
}, nil)
// 2. prekey count query (whatsmeow prekeys.go shape).
add("encrypt_count", waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{"id": id, "xmlns": "encrypt", "type": "get", "to": types.ServerJID},
Content: []waBinary.Node{{Tag: "count"}},
}, []string{"count"})
// 3. error results.
add("iq_error_404", waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{"id": id, "type": "error", "from": types.ServerJID},
Content: []waBinary.Node{{Tag: "error",
Attrs: waBinary.Attrs{"code": 404, "text": "item-not-found"}}},
}, []string{"error"})
add("iq_error_500", waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{"id": id, "type": "error", "from": types.ServerJID},
Content: []waBinary.Node{{Tag: "error",
Attrs: waBinary.Attrs{"code": 500, "text": "internal-server-error"}}},
}, []string{"error"})
// 4. bare error result (no <error> child: code 0 path).
add("iq_error_bare", waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{"id": id, "type": "error", "from": types.ServerJID},
}, nil)
// 5. pong result.
add("iq_result_ping", waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{"id": id, "type": "result", "from": types.ServerJID},
Content: []waBinary.Node{{Tag: "ping"}},
}, []string{"ping"})
// 6. disconnect shapes (whatsmeow request.go isDisconnectNode inputs).
add("stream_error_401", waBinary.Node{
Tag: "stream:error",
Attrs: waBinary.Attrs{"code": "401"},
}, nil)
add("stream_error_conflict", waBinary.Node{
Tag: "stream:error",
Content: []waBinary.Node{{Tag: "conflict",
Attrs: waBinary.Attrs{"type": "replaced"}}},
}, []string{"conflict"})
add("stream_error_515", waBinary.Node{
Tag: "stream:error",
Attrs: waBinary.Attrs{"code": "515"},
}, nil)
var enc []encodeVec
enc = append(enc, encodeVec{Name: "ping_child", Tag: "ping",
WireHex: mustMarshal(waBinary.Node{Tag: "ping"})})
enc = append(enc, encodeVec{Name: "iq_id_only", Tag: "iq",
Attrs: map[string]any{"id": id},
WireHex: mustMarshal(waBinary.Node{Tag: "iq", Attrs: waBinary.Attrs{"id": id}})})
enc = append(enc, encodeVec{Name: "error_code_text", Tag: "error",
Attrs: map[string]any{"code": "404", "text": "item-not-found"},
WireSet: marshalSet(waBinary.Node{Tag: "error",
Attrs: waBinary.Attrs{"code": 404, "text": "item-not-found"}})})
enc = append(enc, encodeVec{Name: "iq_id_type", Tag: "iq",
Attrs: map[string]any{"id": id, "type": "get"},
WireSet: marshalSet(waBinary.Node{Tag: "iq",
Attrs: waBinary.Attrs{"id": id, "type": "get"}})})
// NOTE: no "to": ServerJID encode vector. whatsmeow passes `to` as a typed
// JID and writes the JID_PAIR form (fa 00 03); detz passes the same value
// as a string and writes its single-byte token (03). Both are valid and
// decode identically (see the keepalive_ping decode vector); the server
// accepts ours (proven live). Byte-parity here would mean knowing which
// strings are JIDs, which the string-attr model cannot express.
// NOTE: no 4-attribute encode vector: Go map iteration covers only a few
// of the 24 orders, so set membership would assert a non-requirement.
// Multi-attribute nodes are decode-direction only (above); the full ping
// shape is additionally covered by live server interop.
vec := map[string]any{"decode": dec, "encode": enc}
out := json.NewEncoder(os.Stdout)
out.SetEscapeHTML(false)
if err := out.Encode(vec); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
@@ -0,0 +1 @@
{"decode":[{"name":"keepalive_ping","wire_hex":"00f8091908ff0342b7a91657042911fa0003","tag":"iq","attrs":{"id":"42.7-9","to":"s.whatsapp.net","type":"get","xmlns":"w:p"},"children":null,"code":0,"text":"","has_error":false},{"name":"encrypt_count","wire_hex":"00f80a1908ff0342b7a916cb042911fa0003f801f80141","tag":"iq","attrs":{"id":"42.7-9","to":"s.whatsapp.net","type":"get","xmlns":"encrypt"},"children":["count"],"code":0,"text":"","has_error":false},{"name":"iq_error_404","wire_hex":"00f8081908ff0342b7a904a806fa0003f801f805a870ec3538b6","tag":"iq","attrs":{"from":"s.whatsapp.net","id":"42.7-9","type":"error"},"children":["error"],"code":404,"text":"item-not-found","has_error":true},{"name":"iq_error_500","wire_hex":"00f8081908ff0342b7a904a806fa0003f801f805a870eca438fc15696e7465726e616c2d7365727665722d6572726f72","tag":"iq","attrs":{"from":"s.whatsapp.net","id":"42.7-9","type":"error"},"children":["error"],"code":500,"text":"internal-server-error","has_error":true},{"name":"iq_error_bare","wire_hex":"00f8071906fa000308ff0342b7a904a8","tag":"iq","attrs":{"from":"s.whatsapp.net","id":"42.7-9","type":"error"},"children":null,"code":0,"text":"","has_error":false},{"name":"iq_result_ping","wire_hex":"00f8081908ff0342b7a9041406fa0003f801f80156","tag":"iq","attrs":{"from":"s.whatsapp.net","id":"42.7-9","type":"result"},"children":["ping"],"code":0,"text":"","has_error":false},{"name":"stream_error_401","wire_hex":"00f8039d708d","tag":"stream:error","attrs":{"code":"401"},"children":null,"code":0,"text":"","has_error":false},{"name":"stream_error_conflict","wire_hex":"00f8029df801f803ec0e04ec11","tag":"stream:error","attrs":{},"children":["conflict"],"code":0,"text":"","has_error":false},{"name":"stream_error_515","wire_hex":"00f8039d70ff82515f","tag":"stream:error","attrs":{"code":"515"},"children":null,"code":0,"text":"","has_error":false}],"encode":[{"name":"ping_child","tag":"ping","attrs":null,"wire_hex":"00f80156"},{"name":"iq_id_only","tag":"iq","attrs":{"id":"42.7-9"},"wire_hex":"00f8031908ff0342b7a9"},{"name":"error_code_text","tag":"error","attrs":{"code":"404","text":"item-not-found"},"wire_set":["00f805a838b670ec35","00f805a870ec3538b6"]},{"name":"iq_id_type","tag":"iq","attrs":{"id":"42.7-9","type":"get"},"wire_set":["00f80519042908ff0342b7a9","00f8051908ff0342b7a90429"]}]}
+100
View File
@@ -0,0 +1,100 @@
// Oracle generator for detz::wa token-dictionary freshness vectors (M1).
//
// Dumps whatsmeow's own binary/token tables (pinned version, see
// docs/whatsapp.md section 17) plus whatsmeow binary.Marshal wire bytes for
// token-probe nodes, so the C++ tables in src/whatsapp/wa_tokens_data.h and
// the codec's token selection are checked against the reference, not against
// themselves. Re-running this after a pin move fails loudly on drift.
//
// Run from a module that requires go.mau.fi/whatsmeow:
// go run token_oracle.go > token_vectors.json (inside a module that requires go.mau.fi/whatsmeow)
//go:build ignore
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/binary/token"
)
type probe struct {
String string `json:"string"`
WireHex string `json:"wire_hex"`
// Index expectation from whatsmeow itself (single >= 0, else dict/index).
Single int `json:"single"`
Dict int `json:"dict"`
Index int `json:"index"`
Found bool `json:"found"`
}
func main() {
// Full tables, verbatim.
single := make([]string, len(token.SingleByteTokens))
copy(single, token.SingleByteTokens[:])
double := make([][]string, len(token.DoubleByteTokens))
for d, dict := range token.DoubleByteTokens {
double[d] = make([]string, len(dict))
copy(double[d], dict[:])
}
// Probe strings: single-byte tokens, one token from each double-byte
// dictionary, and strings with no token (literal encoding).
probes := []string{
"iq", "s.whatsapp.net", "w:p", "message", "lid",
"patch", "mute_v2",
"pair-device", "phash",
"mediaretry", "enc_p",
"create", "spam_request",
"no-such-token-in-any-dict-zzz",
}
var out []probe
for _, s := range probes {
p := probe{String: s, Single: -1, Dict: -1, Index: -1}
if v, ok := token.IndexOfSingleToken(s); ok {
p.Single, p.Found = int(v), true
} else if d, i, ok := token.IndexOfDoubleByteToken(s); ok {
p.Dict, p.Index, p.Found = int(d), int(i), true
}
// A bare tag node has no attributes, so whatsmeow's marshal is
// deterministic here and the C++ encoder must match it byte-for-byte.
raw, err := waBinary.Marshal(waBinary.Node{Tag: s})
if err != nil {
fmt.Fprintf(os.Stderr, "marshal %q: %v\n", s, err)
os.Exit(1)
}
p.WireHex = hex.EncodeToString(raw)
out = append(out, p)
}
// A GetDoubleToken miss must be an error, never a panic.
if _, err := token.GetDoubleToken(4, 0); err == nil {
fmt.Fprintln(os.Stderr, "GetDoubleToken(4,0) unexpectedly succeeded")
os.Exit(1)
}
vec := map[string]any{
"dict_version": token.DictVersion,
"single": single,
"double": double,
"consts": map[string]int{
"ListEmpty": token.ListEmpty, "Dictionary0": token.Dictionary0,
"Dictionary1": token.Dictionary1, "Dictionary2": token.Dictionary2,
"Dictionary3": token.Dictionary3, "InteropJID": token.InteropJID,
"FBJID": token.FBJID, "ADJID": token.ADJID, "List8": token.List8,
"List16": token.List16, "JIDPair": token.JIDPair, "Hex8": token.Hex8,
"Binary8": token.Binary8, "Binary20": token.Binary20,
"Binary32": token.Binary32, "Nibble8": token.Nibble8,
"PackedMax": token.PackedMax, "SingleByteMax": token.SingleByteMax,
},
"probes": out,
}
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
if err := enc.Encode(vec); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
File diff suppressed because one or more lines are too long
+377
View File
@@ -15,9 +15,13 @@
#include "json.h"
#include "wa_binary.h"
#include "wa_cert.h"
#include "wa_client.h"
#include "wa_consts.h"
#include "wa_crypto.h"
#include "wa_device.h"
#include "wa_iq.h"
#include "wa_keepalive.h"
#include "wa_noise.h"
#include "wa_proto.h"
#include "wa_signal.h"
@@ -25,6 +29,7 @@
#include "wa_media.h"
#include "wa_receiver.h"
#include "wa_store.h"
#include "wa_tokens_data.h"
#include "wa_vcf.h"
using namespace detz::wa;
@@ -347,6 +352,29 @@ static void test_hostile() {
}
}
check(ok, "2000 random inputs: no length_error, no crash");
// 5. M1: frame reassembly with hostile cursor/length values yields "no
// frame yet", never a crash and never a wraparound slice.
ok = true;
try {
Bytes buf = {0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o'};
size_t pos = 99; // cursor past the end: caller bug, still handled
if (WaFraming::unframe(buf, pos).has_value()) ok = false;
pos = 6; // 2 bytes left, fewer than the 3-byte length prefix
if (WaFraming::unframe(buf, pos).has_value() || pos != 6) ok = false;
Bytes huge = {0xff, 0xff, 0xff}; // length 16M-1 claimed, 0 bytes present
pos = 0;
if (WaFraming::unframe(huge, pos).has_value() || pos != 0) ok = false;
Bytes empty;
pos = 0;
if (WaFraming::unframe(empty, pos).has_value()) ok = false;
pos = 0; // exact frame still parses after the hostile attempts
auto fr = WaFraming::unframe(buf, pos);
if (!fr || fr->size() != 5 || pos != 8) ok = false;
} catch (const std::exception&) {
ok = false; // unframe never throws; incomplete input is a routine negative
}
check(ok, "hostile frame cursors/lengths yield no frame, never crash");
}
// ---- device serialize round-trip (regression for incident WA-2026-10-27) ----
@@ -862,8 +890,357 @@ static void test_vcf_import() {
std::filesystem::remove_all(home);
}
// ---- M1: token dictionary freshness (whatsmeow token_oracle.go) ----
static void test_tokens() {
std::printf("[tokens]\n");
Value d = Value::loads(slurp(path("token_vectors.json")));
check(d.get("dict_version").as_int(0) == kDictVersion, "dict version is whatsmeow DictVersion (3)");
const Value& single = d.get("single");
bool sok = single.is_array() && single.size() == kSingleByteTokens.size();
for (size_t i = 0; sok && i < kSingleByteTokens.size(); ++i)
sok = single.at(i).str() == std::string(kSingleByteTokens[i]);
check(sok, "236 single-byte tokens match whatsmeow verbatim");
const Value& dbl = d.get("double");
bool dok = dbl.is_array() && dbl.size() == kDoubleByteTokens.size();
for (size_t x = 0; dok && x < kDoubleByteTokens.size(); ++x) {
dok = dbl.at(x).is_array() && dbl.at(x).size() == kDoubleByteTokens[x].size();
for (size_t i = 0; dok && i < kDoubleByteTokens[x].size(); ++i)
dok = dbl.at(x).at(i).str() == std::string(kDoubleByteTokens[x][i]);
}
check(dok, "4x256 double-byte tokens match whatsmeow verbatim");
const Value& c = d.get("consts");
bool cok = c.get("ListEmpty").as_int(-1) == WaBinary::LIST_EMPTY &&
c.get("Dictionary0").as_int(-1) == WaBinary::DICTIONARY_0 &&
c.get("Dictionary1").as_int(-1) == WaBinary::DICTIONARY_1 &&
c.get("Dictionary2").as_int(-1) == WaBinary::DICTIONARY_2 &&
c.get("Dictionary3").as_int(-1) == WaBinary::DICTIONARY_3 &&
c.get("InteropJID").as_int(-1) == WaBinary::INTEROP_JID &&
c.get("FBJID").as_int(-1) == WaBinary::FB_JID && c.get("ADJID").as_int(-1) == WaBinary::AD_JID &&
c.get("List8").as_int(-1) == WaBinary::LIST_8 && c.get("List16").as_int(-1) == WaBinary::LIST_16 &&
c.get("JIDPair").as_int(-1) == WaBinary::JID_PAIR && c.get("Hex8").as_int(-1) == WaBinary::HEX_8 &&
c.get("Binary8").as_int(-1) == WaBinary::BINARY_8 &&
c.get("Binary20").as_int(-1) == WaBinary::BINARY_20 &&
c.get("Binary32").as_int(-1) == WaBinary::BINARY_32 &&
c.get("Nibble8").as_int(-1) == WaBinary::NIBBLE_8 &&
c.get("PackedMax").as_int(-1) == WaBinary::PACKED_MAX &&
c.get("SingleByteMax").as_int(-1) == WaBinary::SINGLE_BYTE_MAX;
check(cok, "wire tag consts equal token.* (18 values)");
// Probes: bare-tag nodes are deterministic, so our encoder must match
// whatsmeow's marshal byte-for-byte; every probe also decodes back.
int ok = 0, n = 0;
for (const auto& p : d.get("probes").items()) {
++n;
const std::string s = p.get("string").str();
WaNode back = WaBinary::decode(WaBinary::unpack(fromhex(p.get("wire_hex").str())));
if (back.tag != s) continue;
Bytes raw;
raw.push_back(0);
Bytes enc = WaBinary::encode(WaNode(s));
raw.insert(raw.end(), enc.begin(), enc.end());
if (tohex(raw) == p.get("wire_hex").str()) ++ok;
}
check(ok == n && n > 10, "token probe nodes encode byte-exact (" + std::to_string(ok) + "/" +
std::to_string(n) + ", single + all 4 dicts + literal)");
}
// ---- M1: iq request/response (whatsmeow iq_oracle.go) ----
static void test_iq() {
std::printf("[iq]\n");
Value d = Value::loads(slurp(path("iq_vectors.json")));
// Decode direction: whatsmeow wrote the bytes, we read them.
int ok = 0, n = 0;
for (const auto& v : d.get("decode").items()) {
++n;
WaNode node = WaBinary::decode(WaBinary::unpack(fromhex(v.get("wire_hex").str())));
bool good = node.tag == v.get("tag").str();
for (const auto& m : v.get("attrs").members()) good = good && node.attr(m.first) == m.second.str();
good = good && node.attrs.size() == v.get("attrs").size();
const Value& kids = v.get("children");
for (size_t i = 0; good && i < kids.size(); ++i) good = node.child(kids.at(i).str()) != nullptr;
WaIqError err = WaIq::parse_error(node);
good = good && err.code() == (int)v.get("code").as_int(0) && err.text() == v.get("text").str() &&
err.has_error_node() == v.get("has_error").as_bool();
if (good) ++ok;
}
check(ok == n && n == 9, "iq nodes decode + classify like whatsmeow (" + std::to_string(ok) + "/" +
std::to_string(n) + ": ping, count, 404, 500, bare, result, 3 disconnects)");
// Encode direction: deterministic nodes must match whatsmeow's marshal bytes.
int eok = 0, en = 0;
for (const auto& v : d.get("encode").items()) {
++en;
WaNode node(v.get("tag").str());
for (const auto& m : v.get("attrs").members()) node.attrs[m.first] = m.second.str();
Bytes raw;
raw.push_back(0);
Bytes enc = WaBinary::encode(node);
raw.insert(raw.end(), enc.begin(), enc.end());
const std::string got = tohex(raw);
if (v.has("wire_hex")) {
if (got == v.get("wire_hex").str()) ++eok;
} else {
for (const auto& w : v.get("wire_set").items())
if (got == w.str()) {
++eok;
break;
}
}
}
check(eok == en && en == 4, "iq nodes encode within whatsmeow's bytes (" + std::to_string(eok) + "/" +
std::to_string(en) + ": exact x2, order-set x2)");
// JID-valued attrs have two valid wire forms: whatsmeow writes its typed
// JID as JID_PAIR, we write the same string as its single-byte token; the
// decode checks above prove both read back to "s.whatsapp.net".
WaNode to_form("iq");
to_form.attrs["to"] = "s.whatsapp.net";
Bytes to_raw = WaBinary::encode(to_form);
check(to_raw.size() == 5 && to_raw[3] == 0x11 && to_raw[4] == 0x03,
"our server-JID attr encodes as the 1-byte token (compact valid form)");
// WaIq::build assembles the ping exactly as whatsmeow sendIQAsyncAndGetData does.
WaIqQuery q;
q.ns = "w:p";
q.type = "get";
q.to = WaConsts::kServerJid;
q.id = "42.7-9";
WaNode ping = WaIq::build(q);
check(ping.tag == "iq" && ping.attr("id") == "42.7-9" && ping.attr("xmlns") == "w:p" &&
ping.attr("type") == "get" && ping.attr("to") == "s.whatsapp.net" &&
!ping.has_attr("smax_id") && !ping.has_attr("target"),
"WaIq::build shapes the keepalive ping (id/xmlns/type/to, no smax/target)");
WaIqQuery qs = q;
qs.smax_id = "99";
qs.target = "31611111111@s.whatsapp.net";
WaNode with_extra = WaIq::build(qs);
check(with_extra.attr("smax_id") == "99" && with_extra.attr("target") == "31611111111@s.whatsapp.net",
"WaIq::build carries smax_id + target when set");
// Request ids mirror whatsmeow "<byte0>.<byte1>-<counter>".
check(WaIq::generate_id("42.7-", 9) == "42.7-9", "request id is prefix + counter");
std::string pfx = WaIq::new_prefix();
check(pfx.size() >= 5 && pfx.back() == '-' && pfx.find('.') != std::string::npos,
"request prefix has the <n>.<m>- shape");
check(WaIq::new_prefix().size() >= 5, "a second prefix draws cleanly");
// Disconnect predicates on the oracle's disconnect nodes.
const Value& dec = d.get("decode");
auto by_name = [&](const std::string& name) {
for (const auto& v : dec.items())
if (v.get("name").str() == name) return WaBinary::decode(WaBinary::unpack(fromhex(v.get("wire_hex").str())));
return WaNode("missing");
};
WaNode e401 = by_name("stream_error_401"), conf = by_name("stream_error_conflict"),
e515 = by_name("stream_error_515"), pong = by_name("iq_result_ping");
check(WaIq::is_disconnect_node(e401) && WaIq::is_disconnect_node(conf) && WaIq::is_disconnect_node(e515) &&
!WaIq::is_disconnect_node(pong),
"disconnect nodes recognized (stream:error yes, iq result no)");
check(WaIq::is_auth_error_disconnect(e401) && WaIq::is_auth_error_disconnect(conf) &&
!WaIq::is_auth_error_disconnect(e515) && !WaIq::is_auth_error_disconnect(pong),
"auth disconnects (401, replaced) separated from the 515 restart");
WaNode xml_end("xmlstreamend");
check(WaIq::is_disconnect_node(xml_end) && !WaIq::is_auth_error_disconnect(xml_end),
"xmlstreamend disconnects without being an auth error");
// Error taxonomy mirrors whatsmeow errors.go.
WaIqError e404 = WaIq::parse_error(by_name("iq_error_404"));
check(e404.is(WaIqError::not_found()) && !e404.is(WaIqError::internal_server_error()),
"parsed 404 is ErrIQNotFound, not ErrIQInternalServerError");
check(WaIqError::timed_out().message() == "info query timed out", "timeout message matches ErrIQTimedOut");
check(WaIqError::not_found().message() == "info query returned status 404: item-not-found",
"status message matches IQError.Error");
check(WaDisconnectedError::for_iq().message() == "websocket disconnected before info query returned response",
"disconnect message matches DisconnectedError.Error");
check(WaDisconnectedError::for_iq().is(WaDisconnectedError("info query")) &&
!WaDisconnectedError::for_iq().is(WaDisconnectedError("message send")),
"disconnect equivalence is by action");
WaIqError bare = WaIq::parse_error(by_name("iq_error_bare"));
check(bare.code() == 0 && !bare.has_error_node(), "a result without <error> parses to code 0");
// Hostile shapes: never throw, never overflow, code reads 0.
WaNode evil("iq");
evil.content = WaNode::Children{WaNode("error")};
WaNode* child = const_cast<WaNode*>(evil.child("error"));
child->attrs["code"] = "not-a-number";
check(WaIq::parse_error(evil).code() == 0, "a non-numeric error code reads 0");
child->attrs["code"] = "99999999999999999999999";
check(WaIq::parse_error(evil).code() == 0, "an overflowing error code reads 0, no UB");
child->attrs.erase("code");
child->attrs["text"] = std::string(70000, 'x');
check(WaIq::parse_error(evil).code() == 0, "a missing code with a huge text reads 0");
// Socket guards fail loudly without any network.
bool threw = false;
try {
WaSocket s;
s.send_frame(Bytes{1, 2, 3});
} catch (const WaSocketError&) {
threw = true;
}
check(threw, "sending on an unconnected socket throws WaSocketError");
WaSocketStatusError with_status("connect: refused", 403);
check(with_status.status() == 403 && std::string(with_status.what()).find("403") != std::string::npos,
"the status-carrying socket error keeps its code (ErrWithStatusCode role)");
}
// ---- M1: keepalive schedule (deterministic clock) ----
static void test_keepalive() {
std::printf("[keepalive]\n");
check(WaConsts::kWaKeepAliveResponseDeadline == 10 && WaConsts::kWaKeepAliveIntervalMin == 20 &&
WaConsts::kWaKeepAliveIntervalMax == 30 && WaConsts::kWaKeepAliveMaxFailTime == 180,
"keepalive caps equal whatsmeow (10s deadline, 20-30s jitter, 3min horizon)");
WaKeepAlive ka([]() { return 25000; });
ka.start(100000);
check(!ka.due(100000) && !ka.due(124999) && ka.due(125000), "first ping due after the interval");
check(ka.error_count() == 0, "no errors at start");
ka.record_pong(125000);
check(ka.error_count() == 0 && ka.last_success_ms() == 125000 && !ka.due(149999) && ka.due(150000),
"a pong clears errors and advances the schedule");
check(!ka.record_timeout(150000) && ka.error_count() == 1, "one unanswered ping counts, no reconnect yet");
check(!ka.record_timeout(175000) && ka.error_count() == 2, "errors accumulate across pings");
ka.record_pong(200000);
check(ka.error_count() == 0 && ka.last_success_ms() == 200000, "a later pong recovers the streak");
// Failures spanning more than 3 minutes force a reconnect (whatsmeow KeepAliveMaxFailTime).
WaKeepAlive dying([]() { return 25000; });
dying.start(0);
bool forced = false;
for (int64_t t = 25000; t <= 400000 && !forced; t += 25000) forced = dying.record_timeout(t);
check(forced && dying.error_count() == 8, "8 unanswered pings (200s of failure) force a reconnect");
WaKeepAlive edge([]() { return 25000; });
edge.start(0);
bool early = true;
for (int i = 0; i < 7; ++i) early = early && !edge.record_timeout(25000 + i * 25000);
check(early && edge.record_timeout(200000), "the horizon trips past 180s, not before");
// The default jitter always stays inside 20-30s.
bool in_range = true;
for (int i = 0; i < 200; ++i) {
WaKeepAlive probe;
probe.start(0);
int64_t wait = probe.next_due_ms();
in_range = in_range && wait >= 20000 && wait <= 30000;
}
check(in_range, "default jitter stays inside 20-30s over 200 draws");
check(WaConsts::kWaIqDefaultTimeout == 75, "default iq wait is whatsmeow defaultRequestTimeout (75s)");
}
// ---- M1: Noise parity + server certificate (whatsmeow cert_oracle.go) ----
static void test_server_cert() {
std::printf("[server-cert]\n");
Value d = Value::loads(slurp(path("cert_vectors.json")));
Value n = Value::loads(slurp(path("noise.json")));
// Re-run the handshake over the wa-go trace: our decrypted bytes must equal
// whatsmeow's own NoiseHandshake output from the oracle.
auto ephPriv = a32(fromhex(n.get("ephemeralKeyPriv").str()));
auto ephPub = a32(fromhex(n.get("ephemeralKeyPub").str()));
auto stPriv = a32(fromhex(n.get("authStaticKeyPriv").str()));
auto stPub = a32(fromhex(n.get("authStaticKeyPub").str()));
Bytes shFrame = fromhex(n.get("serverHelloFrameHex").str());
size_t pos = 0;
auto sh = WaFraming::unframe(shFrame, pos);
WaNoise noise(ephPub);
auto res = noise.client_handshake(ephPriv, ephPub, stPriv, stPub, *sh, Bytes{1, 2, 3, 4});
check(res.has_value(), "handshake completes (mix_shared_secret refactor)");
check(res && tohex(res->server_static) == d.get("server_static_hex").str(),
"server static equals whatsmeow NoiseHandshake output");
check(res && tohex(res->server_payload) == d.get("server_payload_hex").str(),
"server payload equals whatsmeow NoiseHandshake output (241 bytes)");
// Details fields match whatsmeow's waCert parse field by field.
auto chain = parse_cert_chain(fromhex(d.get("server_payload_hex").str()));
auto im = parse_cert_details(chain->intermediate_details);
auto leaf = parse_cert_details(chain->leaf_details);
const Value& oi = d.get("intermediate");
const Value& ol = d.get("leaf");
check(im && im->serial == (uint32_t)oi.get("serial").as_int(0) &&
im->issuer_serial == (uint32_t)oi.get("issuer_serial").as_int(0) &&
tohex(im->key) == oi.get("key_hex").str() &&
im->not_before == (uint64_t)oi.get("not_before").as_int(0) &&
im->not_after == (uint64_t)oi.get("not_after").as_int(0),
"intermediate details match whatsmeow (serial 3, issuer 0, key, window)");
check(leaf && leaf->serial == (uint32_t)ol.get("serial").as_int(0) &&
leaf->issuer_serial == (uint32_t)ol.get("issuer_serial").as_int(0) &&
tohex(leaf->key) == ol.get("key_hex").str() &&
leaf->not_before == (uint64_t)ol.get("not_before").as_int(0) &&
leaf->not_after == (uint64_t)ol.get("not_after").as_int(0),
"leaf details match whatsmeow (serial 354, issuer 3, key, window)");
Bytes pubkey(WaConsts::kWaCertPubKey.begin(), WaConsts::kWaCertPubKey.end());
check(tohex(pubkey) == d.get("wacert_pubkey_hex").str(), "pinned key equals whatsmeow WACertPubKey");
check(WaConsts::kWaCertIssuerSerial == 0, "pinned issuer serial equals whatsmeow WACertIssuerSerial");
// The captured leaf is expired today, so travel inside its window: every
// check must pass there (the oracle confirms the signatures are genuine).
Bytes payload = fromhex(d.get("server_payload_hex").str());
Bytes server_static = fromhex(d.get("server_static_hex").str());
int64_t mid = (int64_t)(((uint64_t)ol.get("not_before").as_int(0) + (uint64_t)ol.get("not_after").as_int(0)) / 2);
bool accepted = false;
try {
WaServerCert::verify(payload, server_static, mid);
accepted = true;
} catch (const std::exception&) {
}
check(accepted, "a genuine chain verifies inside its validity window");
bool expired = false;
try {
WaServerCert::verify(payload, server_static);
} catch (const WaCertError& e) {
expired = std::string(e.what()).find("expired") != std::string::npos;
}
check(expired, "the same chain is refused as expired today");
// Tamper with every verified property: each must fail closed with a reason.
auto tampers = [&](const std::string& name, const Bytes& bad_payload, const Bytes& bad_static) {
bool threw = false;
try {
WaServerCert::verify(bad_payload, bad_static, mid);
} catch (const WaCertError&) {
threw = true;
} catch (const std::length_error&) {
threw = false;
}
check(threw, "tampered chain refused: " + name);
};
Bytes bad_sig = payload;
bad_sig[bad_sig.size() - 1] ^= 0x01;
tampers("flipped signature byte", bad_sig, server_static);
Bytes bad_static = server_static;
bad_static[0] ^= 0x01;
tampers("wrong server static", payload, bad_static);
tampers("truncated chain", Bytes(payload.begin(), payload.begin() + payload.size() / 2), server_static);
tampers("empty chain", Bytes{}, server_static);
tampers("empty static", payload, Bytes{});
tampers("garbage chain", Bytes(241, 0xab), server_static);
// Hostile protobuf inside the chain: oversized lengths and overlong varints
// must throw a handled error, never length_error, never UB.
bool hostile_ok = true;
for (const Bytes& evil : {Bytes{0x0a, 0xff, 0xff, 0xff, 0xff, 0x0f}, Bytes{0x12, 0x80, 0x80, 0x80, 0x80,
0x80, 0x80, 0x80, 0x80, 0x80, 0x7f},
Bytes{0x0a}, Bytes{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}}) {
try {
WaServerCert::verify(evil, server_static, mid);
} catch (const std::length_error&) {
hostile_ok = false;
} catch (const std::exception&) {
}
}
check(hostile_ok, "hostile chain bytes rejected without length_error");
// A swapped leaf/intermediate must not verify (regression: the fields were swapped).
WaCertChainParsed swapped;
swapped.leaf_details = chain->intermediate_details;
swapped.leaf_signature = chain->intermediate_signature;
swapped.intermediate_details = chain->leaf_details;
swapped.intermediate_signature = chain->leaf_signature;
// Re-encode the swap through the writer to feed verify() bytes.
ProtoWriter wl, wi, wc;
wl.message(1, swapped.leaf_details);
wl.bytes(2, swapped.leaf_signature);
wi.message(1, swapped.intermediate_details);
wi.bytes(2, swapped.intermediate_signature);
wc.message(1, wl.take());
wc.message(2, wi.take());
bool swap_threw = false;
try {
WaServerCert::verify(wc.take(), server_static, mid);
} catch (const WaCertError&) {
swap_threw = true;
}
check(swap_threw, "a leaf/intermediate swap is refused");
}
int main(int argc, char** argv) {
DIR = argc > 1 ? argv[1] : "tests/whatsapp_data";
test_tokens();
test_iq();
test_keepalive();
test_server_cert();
test_contact_search();
test_device_props();
test_crypto();
+11 -8
View File
@@ -4,8 +4,8 @@
# for detz::wa's parsing layer: a malformed frame that overflows a length check,
# over-reads a buffer, or over-allocates a vector aborts here at the exact line,
# instead of slipping past a bounds check in a release build. Run it in CI and
# before any change to a parser (WaBinary, WaProto, WaNoise, WaSignal). See
# docs/cpp-safety.md.
# before any change to a parser (WaBinary, WaProto, WaNoise, WaSignal, WaIq,
# WaServerCert). See docs/cpp-safety.md.
#
# The vendored ed25519 ref10 (third_party/ed25519) is built WITHOUT UBSan on
# purpose: it uses intentional, portable signed-shift idioms that UBSan flags
@@ -15,17 +15,20 @@ cd "$(dirname "$0")/.."
OUT=$(mktemp -d)
trap 'rm -rf "$OUT"' EXIT
SAN="-fsanitize=address,undefined -fno-sanitize-recover=all -D_GLIBCXX_ASSERTIONS -g -O1 -std=c++23 -Wall -Wextra"
INC="-Isrc/whatsapp -Isrc/llm -Ithird_party/ed25519"
# only the parser TUs the hostile/golden test exercises
for f in wa_crypto wa_binary wa_proto wa_noise wa_signal wa_device; do
g++ $SAN $INC -c "src/whatsapp/$f.cpp" -o "$OUT/$f.o"
INC="-Isrc/whatsapp -Isrc/llm -Isrc/seal -Isrc/store -Isrc/util -Ithird_party/ed25519 -Ithird_party/qrcodegen"
# the whole detz::wa module plus the support TUs the test links (json, vault,
# base64, fuzzy): the hostile/golden test exercises the parsers through them.
n=0
for f in src/whatsapp/*.cpp src/llm/json.cpp src/seal/seal_base64.cpp src/store/sqldb.cpp src/util/fuzzy.cpp \
third_party/qrcodegen/*.cpp; do
n=$((n + 1))
g++ $SAN $INC -c "$f" -o "$OUT/tu$n.o"
done
g++ $SAN $INC -c src/llm/json.cpp -o "$OUT/json.o"
# ref10 C without UBSan (intentional portable shift idioms), still ASan for memory
for f in third_party/ed25519/*.c; do
cc -fsanitize=address -D_GLIBCXX_ASSERTIONS -g -O2 -Ithird_party/ed25519 -c "$f" -o "$OUT/$(basename "$f" .c).o"
done
g++ $SAN $INC tests/whatsapp_test.cpp "$OUT"/*.o -lssl -lcrypto -lz -o "$OUT/whatsapp_test_san"
g++ $SAN $INC tests/whatsapp_test.cpp "$OUT"/*.o -lssl -lcrypto -lz -lcurl -lsqlite3 -o "$OUT/whatsapp_test_san"
echo "running whatsapp_test under ASan+UBSan+_GLIBCXX_ASSERTIONS ..."
"$OUT/whatsapp_test_san" tests/whatsapp_data
echo "san-whatsapp: OK"
+4 -1
View File
@@ -65,9 +65,12 @@ def pkg_of(text):
return m.group(1) if m else "?"
def strip_line(ln):
ln = ln.split("//", 1)[0]
# Strings first, then the comment: a "//" inside a literal (e.g. the
# "https://..." token in binary/token/token.go) must not truncate the
# line, or brace-depth counting breaks and the block swallows the file.
ln = re.sub(r"`[^`]*`", "", ln)
ln = re.sub(r'"(?:[^"\\]|\\.)*"', "", ln)
ln = ln.split("//", 1)[0]
return ln
def exported(name):
+75 -52
View File
@@ -5,6 +5,7 @@
# Mapping rule: a named C++ entity (function, method, type, const, field)
# performs the counterpart role; inline fragments with no named entity stay
# UNMAPPED for a later milestone to name. Subset notes name the gap (M1-M10).
appstate.AllPatchNames UNMAPPED
appstate.BuildArchive UNMAPPED
appstate.BuildDeleteChat UNMAPPED
@@ -450,22 +451,22 @@ richresponse.UnknownViewModel.String UNMAPPED
richresponse.ViewModel UNMAPPED
richresponse.ViewModelContainer UNMAPPED
richresponse.ViewModelContainer.UnmarshalJSON UNMAPPED
socket.DisconnectHandler UNMAPPED
socket.DisconnectHandler detz::wa::WaClient::on_disconnect (src/whatsapp/wa_client.h; invoked when the stream closes)
socket.ErrDialFailed detz::wa::WaSocket::connect throws WaSocketError (src/whatsapp/wa_socket.h; shared type)
socket.ErrFrameTooLarge detz::wa::WaFraming::frame throws on oversize (src/whatsapp/wa_noise.cpp)
socket.ErrSocketAlreadyOpen UNMAPPED
socket.ErrSocketClosed UNMAPPED
socket.ErrWithStatusCode UNMAPPED
socket.FrameHandler UNMAPPED
socket.FrameLengthSize detz::wa::WaFraming::frame/unframe 3-byte prefix (src/whatsapp/wa_noise.h; literal, unnamed)
socket.ErrSocketAlreadyOpen detz::wa::WaSocket::connect throws WaSocketError (src/whatsapp/wa_socket.h; already-open guard)
socket.ErrSocketClosed detz::wa::WaSocket::send_frame throws WaSocketError (src/whatsapp/wa_socket.h; not-connected guard)
socket.ErrWithStatusCode detz::wa::WaSocketStatusError (src/whatsapp/wa_socket.h)
socket.FrameHandler detz::wa::WaClient::recv_node + dispatch (src/whatsapp/wa_client.h; pull model, no frame callback)
socket.FrameLengthSize detz::wa::WaFraming::kLengthSize (src/whatsapp/wa_noise.h)
socket.FrameMaxSize detz::wa::WaFraming::kMaxFrame (src/whatsapp/wa_noise.h)
socket.FrameSocket UNMAPPED
socket.FrameSocket.Close UNMAPPED
socket.FrameSocket.Connect UNMAPPED
socket.FrameSocket.Context UNMAPPED
socket.FrameSocket.IsConnected UNMAPPED
socket.FrameSocket.SendFrame UNMAPPED
socket.NewFrameSocket UNMAPPED
socket.FrameSocket detz::wa::WaSocket (src/whatsapp/wa_socket.h)
socket.FrameSocket.Close detz::wa::WaSocket::close (src/whatsapp/wa_socket.h)
socket.FrameSocket.Connect detz::wa::WaSocket::connect (src/whatsapp/wa_socket.h)
socket.FrameSocket.Context detz::wa::WaSocket::connected (src/whatsapp/wa_socket.h; closure state; the sync model has no cancel contexts)
socket.FrameSocket.IsConnected detz::wa::WaSocket::connected (src/whatsapp/wa_socket.h)
socket.FrameSocket.SendFrame detz::wa::WaSocket::send_frame (src/whatsapp/wa_socket.h)
socket.NewFrameSocket detz::wa::WaSocket::WaSocket (src/whatsapp/wa_socket.h)
socket.NewNoiseHandshake detz::wa::WaNoise::WaNoise (src/whatsapp/wa_noise.h)
socket.NoiseHandshake detz::wa::WaNoise (src/whatsapp/wa_noise.h)
socket.NoiseHandshake.Authenticate detz::wa::WaNoise::mix_hash (src/whatsapp/wa_noise.h; private mix step)
@@ -473,12 +474,12 @@ socket.NoiseHandshake.Decrypt detz::wa::WaNoise::decrypt + transport_decrypt (sr
socket.NoiseHandshake.Encrypt detz::wa::WaNoise::encrypt + transport_encrypt (src/whatsapp/wa_noise.h)
socket.NoiseHandshake.Finish detz::wa::WaNoise::finish (src/whatsapp/wa_noise.h; private split step)
socket.NoiseHandshake.MixIntoKey detz::wa::WaNoise::mix_key (src/whatsapp/wa_noise.h; private)
socket.NoiseHandshake.MixSharedSecretIntoKey UNMAPPED
socket.NoiseHandshake.MixSharedSecretIntoKey detz::wa::WaNoise::mix_shared_secret (src/whatsapp/wa_noise.h)
socket.NoiseHandshake.Start detz::wa::WaNoise::client_handshake (src/whatsapp/wa_noise.h; XX initiator run as one call)
socket.NoiseSocket UNMAPPED
socket.NoiseSocket.IsConnected UNMAPPED
socket.NoiseSocket.SendFrame UNMAPPED
socket.NoiseSocket.Stop UNMAPPED
socket.NoiseSocket detz::wa::WaClient transport pair (src/whatsapp/wa_client.h; WaNoise + WaSocket via send_node/recv_node)
socket.NoiseSocket.IsConnected detz::wa::WaClient::socket_open (src/whatsapp/wa_client.h)
socket.NoiseSocket.SendFrame detz::wa::WaClient::send_node (src/whatsapp/wa_client.h; transport-encrypt + frame send)
socket.NoiseSocket.Stop detz::wa::WaClient::disconnect (src/whatsapp/wa_client.h; close + on_disconnect, no suppress flag)
socket.NoiseStartPattern detz::wa::WaNoise kMode (src/whatsapp/wa_noise.cpp; same 32-byte string)
socket.Origin detz::wa::WaSocket::kOrigin (src/whatsapp/wa_socket.h)
socket.URL detz::wa::WaSocket::kUrl (src/whatsapp/wa_socket.h)
@@ -712,8 +713,30 @@ store.WAVersionContainer.IsZero UNMAPPED
store.WAVersionContainer.LessThan UNMAPPED
store.WAVersionContainer.ProtoAppVersion UNMAPPED
store.WAVersionContainer.String UNMAPPED
token.DoubleByteTokens UNMAPPED
token.SingleByteTokens UNMAPPED
token.ADJID detz::wa::WaBinary::AD_JID (src/whatsapp/wa_binary.h)
token.Binary20 detz::wa::WaBinary::BINARY_20 (src/whatsapp/wa_binary.h)
token.Binary32 detz::wa::WaBinary::BINARY_32 (src/whatsapp/wa_binary.h)
token.Binary8 detz::wa::WaBinary::BINARY_8 (src/whatsapp/wa_binary.h)
token.DictVersion detz::wa::kDictVersion (src/whatsapp/wa_tokens_data.h)
token.Dictionary0 detz::wa::WaBinary::DICTIONARY_0 (src/whatsapp/wa_binary.h)
token.Dictionary1 detz::wa::WaBinary::DICTIONARY_1 (src/whatsapp/wa_binary.h)
token.Dictionary2 detz::wa::WaBinary::DICTIONARY_2 (src/whatsapp/wa_binary.h)
token.Dictionary3 detz::wa::WaBinary::DICTIONARY_3 (src/whatsapp/wa_binary.h)
token.DoubleByteTokens detz::wa::kDoubleByteTokens (src/whatsapp/wa_tokens_data.h)
token.FBJID detz::wa::WaBinary::FB_JID (src/whatsapp/wa_binary.h)
token.GetDoubleToken detz::wa::Decoder::read_string double-token branch (src/whatsapp/wa_binary.cpp; file-local decoder, throws WireError when out of range)
token.Hex8 detz::wa::WaBinary::HEX_8 (src/whatsapp/wa_binary.h)
token.IndexOfDoubleByteToken detz::wa::Encoder::double_token (src/whatsapp/wa_binary.cpp; file-local encoder lookup)
token.IndexOfSingleToken detz::wa::Encoder::single_token (src/whatsapp/wa_binary.cpp; file-local encoder lookup)
token.InteropJID detz::wa::WaBinary::INTEROP_JID (src/whatsapp/wa_binary.h)
token.JIDPair detz::wa::WaBinary::JID_PAIR (src/whatsapp/wa_binary.h)
token.List16 detz::wa::WaBinary::LIST_16 (src/whatsapp/wa_binary.h)
token.List8 detz::wa::WaBinary::LIST_8 (src/whatsapp/wa_binary.h)
token.ListEmpty detz::wa::WaBinary::LIST_EMPTY (src/whatsapp/wa_binary.h)
token.Nibble8 detz::wa::WaBinary::NIBBLE_8 (src/whatsapp/wa_binary.h)
token.PackedMax detz::wa::WaBinary::PACKED_MAX (src/whatsapp/wa_binary.h)
token.SingleByteMax detz::wa::WaBinary::SINGLE_BYTE_MAX (src/whatsapp/wa_binary.h)
token.SingleByteTokens detz::wa::kSingleByteTokens (src/whatsapp/wa_tokens_data.h)
types.AddressingMode UNMAPPED
types.AddressingModeLID UNMAPPED
types.AddressingModePN UNMAPPED
@@ -962,8 +985,8 @@ waArmadilloApplication.Armadillo_Content_RavenMessage.SetImageMessage UNMAPPED
waArmadilloApplication.Armadillo_Content_RavenMessage.SetVideoMessage UNMAPPED
waArmadilloApplication.RavenImageTransportVersion UNMAPPED
waArmadilloApplication.RavenVideoTransportVersion UNMAPPED
waCert.CertChain UNMAPPED
waCert.CertChain_NoiseCertificate_Details UNMAPPED
waCert.CertChain detz::wa::WaCertChainParsed (src/whatsapp/wa_proto.h)
waCert.CertChain_NoiseCertificate_Details detz::wa::WaNoiseCertDetails (src/whatsapp/wa_proto.h)
waCommon.MessageKey UNMAPPED
waCommon.SubProtocol UNMAPPED
waCompanionReg.ClientPairingProps UNMAPPED
@@ -1292,9 +1315,9 @@ whatsmeow.DisappearingTimer24Hours UNMAPPED
whatsmeow.DisappearingTimer7Days UNMAPPED
whatsmeow.DisappearingTimer90Days UNMAPPED
whatsmeow.DisappearingTimerOff UNMAPPED
whatsmeow.DisconnectedError UNMAPPED
whatsmeow.DisconnectedError.Error UNMAPPED
whatsmeow.DisconnectedError.Is UNMAPPED
whatsmeow.DisconnectedError detz::wa::WaDisconnectedError (src/whatsapp/wa_iq.h)
whatsmeow.DisconnectedError.Error detz::wa::WaDisconnectedError::message (src/whatsapp/wa_iq.h)
whatsmeow.DisconnectedError.Is detz::wa::WaDisconnectedError::is (src/whatsapp/wa_iq.h)
whatsmeow.DownloadHTTPError UNMAPPED
whatsmeow.DownloadHTTPError.Error UNMAPPED
whatsmeow.DownloadHTTPError.Is UNMAPPED
@@ -1313,31 +1336,31 @@ whatsmeow.EncSecretPollEdit UNMAPPED
whatsmeow.EncSecretPollVote UNMAPPED
whatsmeow.EncSecretReaction UNMAPPED
whatsmeow.EncSecretReportToken UNMAPPED
whatsmeow.ErrAlreadyConnected UNMAPPED
whatsmeow.ErrAlreadyConnected detz::wa::WaClient::connect refuses when open (src/whatsapp/wa_client.h; loud log + false)
whatsmeow.ErrAppStateUpdate UNMAPPED
whatsmeow.ErrBroadcastListUnsupported UNMAPPED
whatsmeow.ErrBusinessMessageLinkNotFound UNMAPPED
whatsmeow.ErrClientIsNil UNMAPPED
whatsmeow.ErrClientIsNil detz::wa::WaClient (src/whatsapp/wa_client.h; methods take references, a nil receiver is unrepresentable)
whatsmeow.ErrContactQRLinkNotFound UNMAPPED
whatsmeow.ErrEventAlreadyProcessed UNMAPPED
whatsmeow.ErrFileLengthMismatch UNMAPPED
whatsmeow.ErrGroupInviteLinkUnauthorized UNMAPPED
whatsmeow.ErrGroupNotFound UNMAPPED
whatsmeow.ErrIQBadRequest UNMAPPED
whatsmeow.ErrIQDisconnected UNMAPPED
whatsmeow.ErrIQForbidden UNMAPPED
whatsmeow.ErrIQGone UNMAPPED
whatsmeow.ErrIQInternalServerError UNMAPPED
whatsmeow.ErrIQLocked UNMAPPED
whatsmeow.ErrIQNotAcceptable UNMAPPED
whatsmeow.ErrIQNotAllowed UNMAPPED
whatsmeow.ErrIQNotAuthorized UNMAPPED
whatsmeow.ErrIQNotFound UNMAPPED
whatsmeow.ErrIQPartialServerError UNMAPPED
whatsmeow.ErrIQRateOverLimit UNMAPPED
whatsmeow.ErrIQResourceLimit UNMAPPED
whatsmeow.ErrIQServiceUnavailable UNMAPPED
whatsmeow.ErrIQTimedOut UNMAPPED
whatsmeow.ErrIQBadRequest detz::wa::WaIqError::bad_request (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQDisconnected detz::wa::WaDisconnectedError::for_iq (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQForbidden detz::wa::WaIqError::forbidden (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQGone detz::wa::WaIqError::gone (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQInternalServerError detz::wa::WaIqError::internal_server_error (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQLocked detz::wa::WaIqError::locked (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQNotAcceptable detz::wa::WaIqError::not_acceptable (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQNotAllowed detz::wa::WaIqError::not_allowed (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQNotAuthorized detz::wa::WaIqError::not_authorized (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQNotFound detz::wa::WaIqError::not_found (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQPartialServerError detz::wa::WaIqError::partial_server_error (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQRateOverLimit detz::wa::WaIqError::rate_over_limit (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQResourceLimit detz::wa::WaIqError::resource_limit (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQServiceUnavailable detz::wa::WaIqError::service_unavailable (src/whatsapp/wa_iq.h)
whatsmeow.ErrIQTimedOut detz::wa::WaIqError::timed_out (src/whatsapp/wa_iq.h)
whatsmeow.ErrInvalidDisappearingTimer UNMAPPED
whatsmeow.ErrInvalidImageFormat UNMAPPED
whatsmeow.ErrInvalidInlineBotID UNMAPPED
@@ -1357,7 +1380,7 @@ whatsmeow.ErrNoPrivacyToken UNMAPPED
whatsmeow.ErrNoPushName UNMAPPED
whatsmeow.ErrNoSession UNMAPPED
whatsmeow.ErrNoURLPresent UNMAPPED
whatsmeow.ErrNotConnected UNMAPPED
whatsmeow.ErrNotConnected detz::wa::WaSocket::send_frame throws WaSocketError (src/whatsapp/wa_socket.h; not-connected guard)
whatsmeow.ErrNotEncryptedCommentMessage UNMAPPED
whatsmeow.ErrNotEncryptedReactionMessage UNMAPPED
whatsmeow.ErrNotInGroup UNMAPPED
@@ -1401,14 +1424,14 @@ whatsmeow.GetNewsletterUpdatesParams UNMAPPED
whatsmeow.GetProfilePictureParams UNMAPPED
whatsmeow.HashPollOptions UNMAPPED
whatsmeow.IGMessageApplicationVersion UNMAPPED
whatsmeow.IQError UNMAPPED
whatsmeow.IQError.Error UNMAPPED
whatsmeow.IQError.Is UNMAPPED
whatsmeow.IQError detz::wa::WaIqError (src/whatsapp/wa_iq.h)
whatsmeow.IQError.Error detz::wa::WaIqError::message (src/whatsapp/wa_iq.h)
whatsmeow.IQError.Is detz::wa::WaIqError::is (src/whatsapp/wa_iq.h)
whatsmeow.InviteLinkPrefix UNMAPPED
whatsmeow.KeepAliveIntervalMax UNMAPPED
whatsmeow.KeepAliveIntervalMin UNMAPPED
whatsmeow.KeepAliveMaxFailTime UNMAPPED
whatsmeow.KeepAliveResponseDeadline UNMAPPED
whatsmeow.KeepAliveIntervalMax detz::wa::WaConsts::kWaKeepAliveIntervalMax (src/whatsapp/wa_consts.h; schedule in WaKeepAlive, src/whatsapp/wa_keepalive.h)
whatsmeow.KeepAliveIntervalMin detz::wa::WaConsts::kWaKeepAliveIntervalMin (src/whatsapp/wa_consts.h; schedule in WaKeepAlive, src/whatsapp/wa_keepalive.h)
whatsmeow.KeepAliveMaxFailTime detz::wa::WaConsts::kWaKeepAliveMaxFailTime (src/whatsapp/wa_consts.h; schedule in WaKeepAlive, src/whatsapp/wa_keepalive.h)
whatsmeow.KeepAliveResponseDeadline detz::wa::WaConsts::kWaKeepAliveResponseDeadline (src/whatsapp/wa_consts.h; ping in WaClient::send_keepalive_ping)
whatsmeow.MediaAppState UNMAPPED
whatsmeow.MediaAudio UNMAPPED
whatsmeow.MediaConn detz::wa::WaClient::MediaConn (src/whatsapp/wa_client.h; auth+host)
@@ -1493,8 +1516,8 @@ whatsmeow.SendResponse UNMAPPED
whatsmeow.SetProxyOptions UNMAPPED
whatsmeow.UploadResponse detz::wa::WaMedia::Uploaded (src/whatsapp/wa_media.h)
whatsmeow.UsyncQueryExtras UNMAPPED
whatsmeow.WACertIssuerSerial UNMAPPED
whatsmeow.WACertPubKey UNMAPPED
whatsmeow.WACertIssuerSerial detz::wa::WaConsts::kWaCertIssuerSerial (src/whatsapp/wa_consts.h)
whatsmeow.WACertPubKey detz::wa::WaConsts::kWaCertPubKey (src/whatsapp/wa_consts.h; verified by WaServerCert in src/whatsapp/wa_cert.h)
whatsmeow.WantedPreKeyCount detz::wa::WaConsts::kWaPrekeyBatch (src/whatsapp/wa_consts.h)
whatsmeow.WebMessageIDPrefix UNMAPPED
whatsmeow.WebPushConfig UNMAPPED
1 # wa-mapping.tsv: whatsmeow inventory key -> C++ counterpart (M0 baseline).
5 # Mapping rule: a named C++ entity (function, method, type, const, field)
6 # performs the counterpart role; inline fragments with no named entity stay
7 # UNMAPPED for a later milestone to name. Subset notes name the gap (M1-M10).
8 appstate.AllPatchNames
9 appstate.AllPatchNames appstate.BuildArchive
10 appstate.BuildArchive appstate.BuildDeleteChat
11 appstate.BuildDeleteChat appstate.BuildLabelChat
451 richresponse.ViewModel richresponse.ViewModelContainer
452 richresponse.ViewModelContainer richresponse.ViewModelContainer.UnmarshalJSON
453 richresponse.ViewModelContainer.UnmarshalJSON socket.DisconnectHandler
454 socket.DisconnectHandler socket.ErrDialFailed
455 socket.ErrDialFailed socket.ErrFrameTooLarge
456 socket.ErrFrameTooLarge socket.ErrSocketAlreadyOpen
457 socket.ErrSocketAlreadyOpen socket.ErrSocketClosed
458 socket.ErrSocketClosed socket.ErrWithStatusCode
459 socket.ErrWithStatusCode socket.FrameHandler
460 socket.FrameHandler socket.FrameLengthSize
461 socket.FrameLengthSize socket.FrameMaxSize
462 socket.FrameMaxSize socket.FrameSocket
463 socket.FrameSocket socket.FrameSocket.Close
464 socket.FrameSocket.Close socket.FrameSocket.Connect
465 socket.FrameSocket.Connect socket.FrameSocket.Context
466 socket.FrameSocket.Context socket.FrameSocket.IsConnected
467 socket.FrameSocket.IsConnected socket.FrameSocket.SendFrame
468 socket.FrameSocket.SendFrame socket.NewFrameSocket
469 socket.NewFrameSocket socket.NewNoiseHandshake
470 socket.NewNoiseHandshake socket.NoiseHandshake
471 socket.NoiseHandshake socket.NoiseHandshake.Authenticate
472 socket.NoiseHandshake.Authenticate socket.NoiseHandshake.Decrypt
474 socket.NoiseHandshake.Encrypt socket.NoiseHandshake.Finish
475 socket.NoiseHandshake.Finish socket.NoiseHandshake.MixIntoKey
476 socket.NoiseHandshake.MixIntoKey socket.NoiseHandshake.MixSharedSecretIntoKey
477 socket.NoiseHandshake.MixSharedSecretIntoKey socket.NoiseHandshake.Start
478 socket.NoiseHandshake.Start socket.NoiseSocket
479 socket.NoiseSocket socket.NoiseSocket.IsConnected
480 socket.NoiseSocket.IsConnected socket.NoiseSocket.SendFrame
481 socket.NoiseSocket.SendFrame socket.NoiseSocket.Stop
482 socket.NoiseSocket.Stop socket.NoiseStartPattern
483 socket.NoiseStartPattern socket.Origin
484 socket.Origin socket.URL
485 socket.URL socket.WAConnHeader
713 store.WAVersionContainer.LessThan store.WAVersionContainer.ProtoAppVersion
714 store.WAVersionContainer.ProtoAppVersion store.WAVersionContainer.String
715 store.WAVersionContainer.String token.ADJID
716 token.DoubleByteTokens token.Binary20
717 token.SingleByteTokens token.Binary32
718 token.Binary8
719 token.DictVersion
720 token.Dictionary0
721 token.Dictionary1
722 token.Dictionary2
723 token.Dictionary3
724 token.DoubleByteTokens
725 token.FBJID
726 token.GetDoubleToken
727 token.Hex8
728 token.IndexOfDoubleByteToken
729 token.IndexOfSingleToken
730 token.InteropJID
731 token.JIDPair
732 token.List16
733 token.List8
734 token.ListEmpty
735 token.Nibble8
736 token.PackedMax
737 token.SingleByteMax
738 token.SingleByteTokens
739 types.AddressingMode
740 types.AddressingMode types.AddressingModeLID
741 types.AddressingModeLID types.AddressingModePN
742 types.AddressingModePN types.AllowedCredential
985 waArmadilloApplication.Armadillo_Content_RavenMessage.SetVideoMessage waArmadilloApplication.RavenImageTransportVersion
986 waArmadilloApplication.RavenImageTransportVersion waArmadilloApplication.RavenVideoTransportVersion
987 waArmadilloApplication.RavenVideoTransportVersion waCert.CertChain
988 waCert.CertChain waCert.CertChain_NoiseCertificate_Details
989 waCert.CertChain_NoiseCertificate_Details waCommon.MessageKey
990 waCommon.MessageKey waCommon.SubProtocol
991 waCommon.SubProtocol waCompanionReg.ClientPairingProps
992 waCompanionReg.ClientPairingProps waCompanionReg.CompanionCommitment
1315 whatsmeow.DisappearingTimer7Days whatsmeow.DisappearingTimer90Days
1316 whatsmeow.DisappearingTimer90Days whatsmeow.DisappearingTimerOff
1317 whatsmeow.DisappearingTimerOff whatsmeow.DisconnectedError
1318 whatsmeow.DisconnectedError whatsmeow.DisconnectedError.Error
1319 whatsmeow.DisconnectedError.Error whatsmeow.DisconnectedError.Is
1320 whatsmeow.DisconnectedError.Is whatsmeow.DownloadHTTPError
1321 whatsmeow.DownloadHTTPError whatsmeow.DownloadHTTPError.Error
1322 whatsmeow.DownloadHTTPError.Error whatsmeow.DownloadHTTPError.Is
1323 whatsmeow.DownloadHTTPError.Is whatsmeow.DownloadableMessage
1336 whatsmeow.EncSecretPollVote whatsmeow.EncSecretReaction
1337 whatsmeow.EncSecretReaction whatsmeow.EncSecretReportToken
1338 whatsmeow.EncSecretReportToken whatsmeow.ErrAlreadyConnected
1339 whatsmeow.ErrAlreadyConnected whatsmeow.ErrAppStateUpdate
1340 whatsmeow.ErrAppStateUpdate whatsmeow.ErrBroadcastListUnsupported
1341 whatsmeow.ErrBroadcastListUnsupported whatsmeow.ErrBusinessMessageLinkNotFound
1342 whatsmeow.ErrBusinessMessageLinkNotFound whatsmeow.ErrClientIsNil
1343 whatsmeow.ErrClientIsNil whatsmeow.ErrContactQRLinkNotFound
1344 whatsmeow.ErrContactQRLinkNotFound whatsmeow.ErrEventAlreadyProcessed
1345 whatsmeow.ErrEventAlreadyProcessed whatsmeow.ErrFileLengthMismatch
1346 whatsmeow.ErrFileLengthMismatch whatsmeow.ErrGroupInviteLinkUnauthorized
1347 whatsmeow.ErrGroupInviteLinkUnauthorized whatsmeow.ErrGroupNotFound
1348 whatsmeow.ErrGroupNotFound whatsmeow.ErrIQBadRequest
1349 whatsmeow.ErrIQBadRequest whatsmeow.ErrIQDisconnected
1350 whatsmeow.ErrIQDisconnected whatsmeow.ErrIQForbidden
1351 whatsmeow.ErrIQForbidden whatsmeow.ErrIQGone
1352 whatsmeow.ErrIQGone whatsmeow.ErrIQInternalServerError
1353 whatsmeow.ErrIQInternalServerError whatsmeow.ErrIQLocked
1354 whatsmeow.ErrIQLocked whatsmeow.ErrIQNotAcceptable
1355 whatsmeow.ErrIQNotAcceptable whatsmeow.ErrIQNotAllowed
1356 whatsmeow.ErrIQNotAllowed whatsmeow.ErrIQNotAuthorized
1357 whatsmeow.ErrIQNotAuthorized whatsmeow.ErrIQNotFound
1358 whatsmeow.ErrIQNotFound whatsmeow.ErrIQPartialServerError
1359 whatsmeow.ErrIQPartialServerError whatsmeow.ErrIQRateOverLimit
1360 whatsmeow.ErrIQRateOverLimit whatsmeow.ErrIQResourceLimit
1361 whatsmeow.ErrIQResourceLimit whatsmeow.ErrIQServiceUnavailable
1362 whatsmeow.ErrIQServiceUnavailable whatsmeow.ErrIQTimedOut
1363 whatsmeow.ErrIQTimedOut whatsmeow.ErrInvalidDisappearingTimer
1364 whatsmeow.ErrInvalidDisappearingTimer whatsmeow.ErrInvalidImageFormat
1365 whatsmeow.ErrInvalidImageFormat whatsmeow.ErrInvalidInlineBotID
1366 whatsmeow.ErrInvalidInlineBotID whatsmeow.ErrInvalidMediaEncSHA256
1380 whatsmeow.ErrNoPushName whatsmeow.ErrNoSession
1381 whatsmeow.ErrNoSession whatsmeow.ErrNoURLPresent
1382 whatsmeow.ErrNoURLPresent whatsmeow.ErrNotConnected
1383 whatsmeow.ErrNotConnected whatsmeow.ErrNotEncryptedCommentMessage
1384 whatsmeow.ErrNotEncryptedCommentMessage whatsmeow.ErrNotEncryptedReactionMessage
1385 whatsmeow.ErrNotEncryptedReactionMessage whatsmeow.ErrNotInGroup
1386 whatsmeow.ErrNotInGroup whatsmeow.ErrNotLoggedIn
1424 whatsmeow.GetProfilePictureParams whatsmeow.HashPollOptions
1425 whatsmeow.HashPollOptions whatsmeow.IGMessageApplicationVersion
1426 whatsmeow.IGMessageApplicationVersion whatsmeow.IQError
1427 whatsmeow.IQError whatsmeow.IQError.Error
1428 whatsmeow.IQError.Error whatsmeow.IQError.Is
1429 whatsmeow.IQError.Is whatsmeow.InviteLinkPrefix
1430 whatsmeow.InviteLinkPrefix whatsmeow.KeepAliveIntervalMax
1431 whatsmeow.KeepAliveIntervalMax whatsmeow.KeepAliveIntervalMin
1432 whatsmeow.KeepAliveIntervalMin whatsmeow.KeepAliveMaxFailTime
1433 whatsmeow.KeepAliveMaxFailTime whatsmeow.KeepAliveResponseDeadline
1434 whatsmeow.KeepAliveResponseDeadline whatsmeow.MediaAppState
1435 whatsmeow.MediaAppState whatsmeow.MediaAudio
1436 whatsmeow.MediaAudio whatsmeow.MediaConn
1437 whatsmeow.MediaConn whatsmeow.MediaConn.Expiry
1516 whatsmeow.SetProxyOptions whatsmeow.UploadResponse
1517 whatsmeow.UploadResponse whatsmeow.UsyncQueryExtras
1518 whatsmeow.UsyncQueryExtras whatsmeow.WACertIssuerSerial
1519 whatsmeow.WACertIssuerSerial whatsmeow.WACertPubKey
1520 whatsmeow.WACertPubKey whatsmeow.WantedPreKeyCount
1521 whatsmeow.WantedPreKeyCount whatsmeow.WebMessageIDPrefix
1522 whatsmeow.WebMessageIDPrefix whatsmeow.WebPushConfig
1523 whatsmeow.WebPushConfig whatsmeow.WebPushConfig.GetPushConfigAttrs
+42
View File
@@ -0,0 +1,42 @@
# M1 foundations slice (docs/whatsapp.md section 18): the token dictionary,
# the socket/Noise transport, the handshake certificate, the keepalive
# tunables, and the iq error taxonomy. request.go, keepalive.go and
# handshake.go carry no other exported symbols; their behavior lives in
# WaIq, WaKeepAlive and WaServerCert. Deliberately outside M1: the
# KeepAliveTimeout/KeepAliveRestored events (M9 event bus), iq retry on
# reconnect (M9 auto-reconnect), and the binary AttrUtility getters (M4).
pkg:token
pkg:socket
sym:whatsmeow.NoiseHandshakeResponseTimeout
sym:whatsmeow.WACertPubKey
sym:whatsmeow.WACertIssuerSerial
sym:whatsmeow.KeepAliveIntervalMax
sym:whatsmeow.KeepAliveIntervalMin
sym:whatsmeow.KeepAliveMaxFailTime
sym:whatsmeow.KeepAliveResponseDeadline
sym:whatsmeow.IQError
sym:whatsmeow.IQError.Error
sym:whatsmeow.IQError.Is
sym:whatsmeow.DisconnectedError
sym:whatsmeow.DisconnectedError.Error
sym:whatsmeow.DisconnectedError.Is
sym:whatsmeow.ErrIQDisconnected
sym:whatsmeow.ErrIQTimedOut
sym:whatsmeow.ErrNotConnected
sym:whatsmeow.ErrAlreadyConnected
sym:whatsmeow.ErrClientIsNil
sym:whatsmeow.ErrIQBadRequest
sym:whatsmeow.ErrIQNotAuthorized
sym:whatsmeow.ErrIQForbidden
sym:whatsmeow.ErrIQNotFound
sym:whatsmeow.ErrIQNotAllowed
sym:whatsmeow.ErrIQNotAcceptable
sym:whatsmeow.ErrIQGone
sym:whatsmeow.ErrIQResourceLimit
sym:whatsmeow.ErrIQLocked
sym:whatsmeow.ErrIQRateOverLimit
sym:whatsmeow.ErrIQInternalServerError
sym:whatsmeow.ErrIQServiceUnavailable
sym:whatsmeow.ErrIQPartialServerError
sym:waCert.CertChain
sym:waCert.CertChain_NoiseCertificate_Details
+2 -1
View File
@@ -59,7 +59,8 @@ for ln in sys.stdin:
' > "$new_returns"
while IFS= read -r f; do
test -n "$f" || continue
grep -nE "$RET" "$f" | sed "s|^|$f:|" | cut -d: -f1,2 >> "$new_returns"
# A file with no matches must not fail the scan (grep exits 1): it is clean.
grep -nE "$RET" "$f" | sed "s|^|$f:|" | cut -d: -f1,2 >> "$new_returns" || true
done < <(git status --porcelain -- src/whatsapp/ | grep "^??" | sed -e "s|^?? ||" -e "s|^$PREFIX||" || true)
else
echo "wa-stub-scan: no HEAD, scanning the whole tree for silent returns" >&2