@telepapo/sdk 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ [package]
2
+ name = "telepapo-signal"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ publish = false
6
+
7
+ [lib]
8
+ crate-type = ["cdylib", "rlib"]
9
+
10
+ [dependencies]
11
+ libsignal-protocol = { git = "https://github.com/signalapp/libsignal", tag = "v0.105.4" }
12
+ wasm-bindgen = "0.2"
13
+ getrandom = { version = "0.3", features = ["wasm_js"] }
14
+ getrandom02 = { package = "getrandom", version = "0.2", features = ["js"] }
15
+ getrandom04 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
16
+ rand = "0.9"
17
+ futures = { version = "0.3", default-features = false, features = ["executor"] }
18
+ serde = { version = "1", features = ["derive"] }
19
+ serde_json = "1"
20
+ base64 = "0.22"
21
+ async-trait = "0.1"
22
+ uuid = "1"
23
+
24
+ [profile.release]
25
+ opt-level = "s"
26
+ lto = true
@@ -0,0 +1,14 @@
1
+ # Código-fonte do `telepapo_signal_bg.wasm`
2
+
3
+ O WebAssembly em `../` é compilado deste crate Rust, que usa o
4
+ [libsignal](https://github.com/signalapp/libsignal) v0.105.4 (AGPL-3.0).
5
+ Distribuído aqui como a fonte correspondente exigida pela AGPL-3.0.
6
+
7
+ Para compilar (Rust com o alvo `wasm32-unknown-unknown`, `wasm-pack` e `protoc`):
8
+
9
+ ```bash
10
+ RUSTFLAGS='--cfg getrandom_backend="wasm_js"' wasm-pack build --target nodejs --release
11
+ ```
12
+
13
+ O binário publicado foi compilado com `--remap-path-prefix` (os caminhos de
14
+ origem viram `/cargo`, `/rustup` e `/signal-wasm`), o que não muda o código gerado.
@@ -0,0 +1,510 @@
1
+ //! Signal (libsignal 0.105) para o navegador: o mesmo protocolo do app Android,
2
+ //! compilado para WebAssembly.
3
+ //!
4
+ //! O estado inteiro (identidade, pré-chaves, sessões) vive num `Estado`
5
+ //! serializável: o JavaScript chama `exportar()` depois de cada operação que
6
+ //! muda alguma coisa e guarda o JSON no IndexedDB. As lojas do libsignal são
7
+ //! `async`, mas aqui nada espera de verdade — `block_on` resolve na hora.
8
+
9
+ use std::collections::BTreeMap;
10
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
+
12
+ use async_trait::async_trait;
13
+ use base64::{Engine, engine::general_purpose::STANDARD as B64};
14
+ use futures::executor::block_on;
15
+ use libsignal_protocol::{
16
+ CiphertextMessage, DeviceId, Direction, GenericSignedPreKey, IdentityChange, IdentityKey, IdentityKeyPair,
17
+ IdentityKeyStore, KeyPair, KyberPreKeyId, KyberPreKeyRecord, KyberPreKeyStore, PreKeyBundle, PreKeyId,
18
+ PreKeyRecord, PreKeySignalMessage, PreKeyStore, ProtocolAddress, PublicKey, SessionRecord, SessionStore,
19
+ SignalMessage, SignalProtocolError, SignedPreKeyId, SignedPreKeyRecord, SignedPreKeyStore, Timestamp, kem,
20
+ message_decrypt_prekey, message_decrypt_signal, message_encrypt, process_prekey_bundle,
21
+ };
22
+ use rand::{Rng, TryRngCore, rngs::OsRng};
23
+ use serde::{Deserialize, Serialize};
24
+ use wasm_bindgen::prelude::*;
25
+
26
+ type Res<T> = Result<T, SignalProtocolError>;
27
+
28
+ fn erro(e: impl std::fmt::Display) -> JsError {
29
+ JsError::new(&e.to_string())
30
+ }
31
+
32
+ fn b64(b: &[u8]) -> String {
33
+ B64.encode(b)
34
+ }
35
+
36
+ fn de_b64(s: &str) -> Result<Vec<u8>, JsError> {
37
+ B64.decode(s).map_err(erro)
38
+ }
39
+
40
+ /// O relógio vem do JavaScript: `SystemTime::now()` não existe no wasm32.
41
+ fn agora(ms: f64) -> SystemTime {
42
+ UNIX_EPOCH + Duration::from_millis(ms as u64)
43
+ }
44
+
45
+ fn chave_endereco(a: &ProtocolAddress) -> String {
46
+ format!("{}.{}", a.name(), u32::from(a.device_id()))
47
+ }
48
+
49
+ fn endereco(usuario: &str, dispositivo: u32) -> Result<ProtocolAddress, JsError> {
50
+ let d = DeviceId::try_from(dispositivo).map_err(|_| JsError::new("dispositivo inválido"))?;
51
+ Ok(ProtocolAddress::new(usuario.to_owned(), d))
52
+ }
53
+
54
+ #[derive(Serialize, Deserialize, Default, Clone)]
55
+ struct Kyber {
56
+ registro: String,
57
+ ultimo_recurso: bool,
58
+ }
59
+
60
+ #[derive(Serialize, Deserialize, Default, Clone)]
61
+ struct Estado {
62
+ identidade: String,
63
+ registration_id: u32,
64
+ /// Endereço deste navegador (conta + número do aparelho), definido no vínculo.
65
+ usuario: String,
66
+ dispositivo: u32,
67
+ identidades: BTreeMap<String, String>,
68
+ prekeys: BTreeMap<u32, String>,
69
+ signed: BTreeMap<u32, String>,
70
+ kyber: BTreeMap<u32, Kyber>,
71
+ /// Chaves base já vistas com cada Kyber de último recurso (anti-replay).
72
+ kyber_vistos: BTreeMap<String, Vec<String>>,
73
+ sessoes: BTreeMap<String, String>,
74
+ proximo_prekey: u32,
75
+ proximo_signed: u32,
76
+ proximo_kyber: u32,
77
+ /// Contatos que trocaram de identidade (app reinstalado): o JS lê e avisa.
78
+ #[serde(default)]
79
+ trocas: Vec<String>,
80
+ }
81
+
82
+ fn corrompido(o_que: &str) -> SignalProtocolError {
83
+ SignalProtocolError::InvalidState("estado", format!("{o_que} corrompido"))
84
+ }
85
+
86
+ #[async_trait(?Send)]
87
+ impl IdentityKeyStore for Estado {
88
+ async fn get_identity_key_pair(&self) -> Res<IdentityKeyPair> {
89
+ let b = B64.decode(&self.identidade).map_err(|_| corrompido("identidade"))?;
90
+ IdentityKeyPair::try_from(b.as_slice())
91
+ }
92
+
93
+ async fn get_local_registration_id(&self) -> Res<u32> {
94
+ Ok(self.registration_id)
95
+ }
96
+
97
+ async fn save_identity(&mut self, a: &ProtocolAddress, id: &IdentityKey) -> Res<IdentityChange> {
98
+ let nova = b64(&id.serialize());
99
+ let antiga = self.identidades.insert(chave_endereco(a), nova.clone());
100
+ Ok(match antiga {
101
+ Some(v) if v != nova => {
102
+ self.trocas.push(a.name().to_owned());
103
+ IdentityChange::ReplacedExisting
104
+ }
105
+ _ => IdentityChange::NewOrUnchanged,
106
+ })
107
+ }
108
+
109
+ /// Como no app: confia sempre (a troca de identidade vira aviso na conversa).
110
+ async fn is_trusted_identity(&self, _: &ProtocolAddress, _: &IdentityKey, _: Direction) -> Res<bool> {
111
+ Ok(true)
112
+ }
113
+
114
+ async fn get_identity(&self, a: &ProtocolAddress) -> Res<Option<IdentityKey>> {
115
+ match self.identidades.get(&chave_endereco(a)) {
116
+ None => Ok(None),
117
+ Some(v) => {
118
+ let b = B64.decode(v).map_err(|_| corrompido("identidade remota"))?;
119
+ Ok(Some(IdentityKey::decode(&b)?))
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ #[async_trait(?Send)]
126
+ impl PreKeyStore for Estado {
127
+ async fn get_pre_key(&self, id: PreKeyId) -> Res<PreKeyRecord> {
128
+ let v = self.prekeys.get(&u32::from(id)).ok_or(SignalProtocolError::InvalidPreKeyId)?;
129
+ PreKeyRecord::deserialize(&B64.decode(v).map_err(|_| corrompido("prekey"))?)
130
+ }
131
+
132
+ async fn save_pre_key(&mut self, id: PreKeyId, r: &PreKeyRecord) -> Res<()> {
133
+ self.prekeys.insert(u32::from(id), b64(&r.serialize()?));
134
+ Ok(())
135
+ }
136
+
137
+ /// Uso único: usada, apagada (sigilo futuro).
138
+ async fn remove_pre_key(&mut self, id: PreKeyId) -> Res<()> {
139
+ self.prekeys.remove(&u32::from(id));
140
+ Ok(())
141
+ }
142
+ }
143
+
144
+ #[async_trait(?Send)]
145
+ impl SignedPreKeyStore for Estado {
146
+ async fn get_signed_pre_key(&self, id: SignedPreKeyId) -> Res<SignedPreKeyRecord> {
147
+ let v = self.signed.get(&u32::from(id)).ok_or(SignalProtocolError::InvalidSignedPreKeyId)?;
148
+ SignedPreKeyRecord::deserialize(&B64.decode(v).map_err(|_| corrompido("signed"))?)
149
+ }
150
+
151
+ async fn save_signed_pre_key(&mut self, id: SignedPreKeyId, r: &SignedPreKeyRecord) -> Res<()> {
152
+ self.signed.insert(u32::from(id), b64(&r.serialize()?));
153
+ Ok(())
154
+ }
155
+ }
156
+
157
+ #[async_trait(?Send)]
158
+ impl KyberPreKeyStore for Estado {
159
+ async fn get_kyber_pre_key(&self, id: KyberPreKeyId) -> Res<KyberPreKeyRecord> {
160
+ let v = self.kyber.get(&u32::from(id)).ok_or(SignalProtocolError::InvalidKyberPreKeyId)?;
161
+ KyberPreKeyRecord::deserialize(&B64.decode(&v.registro).map_err(|_| corrompido("kyber"))?)
162
+ }
163
+
164
+ async fn save_kyber_pre_key(&mut self, id: KyberPreKeyId, r: &KyberPreKeyRecord) -> Res<()> {
165
+ self.kyber.insert(u32::from(id), Kyber { registro: b64(&r.serialize()?), ultimo_recurso: false });
166
+ Ok(())
167
+ }
168
+
169
+ /// De uso único: apaga. De último recurso: fica, mas a mesma chave base não
170
+ /// pode voltar (seria a mesma mensagem inicial reapresentada).
171
+ async fn mark_kyber_pre_key_used(&mut self, id: KyberPreKeyId, ec: SignedPreKeyId, base: &PublicKey) -> Res<()> {
172
+ let n = u32::from(id);
173
+ let Some(k) = self.kyber.get(&n) else { return Ok(()) };
174
+ if !k.ultimo_recurso {
175
+ self.kyber.remove(&n);
176
+ return Ok(());
177
+ }
178
+ let vistos = self.kyber_vistos.entry(format!("{n}.{}", u32::from(ec))).or_default();
179
+ let base = b64(&base.serialize());
180
+ if vistos.contains(&base) {
181
+ return Err(SignalProtocolError::InvalidMessage(
182
+ libsignal_protocol::CiphertextMessageType::PreKey,
183
+ "reused base key".to_owned(),
184
+ ));
185
+ }
186
+ vistos.push(base);
187
+ Ok(())
188
+ }
189
+ }
190
+
191
+ #[async_trait(?Send)]
192
+ impl SessionStore for Estado {
193
+ async fn load_session(&self, a: &ProtocolAddress) -> Res<Option<SessionRecord>> {
194
+ match self.sessoes.get(&chave_endereco(a)) {
195
+ None => Ok(None),
196
+ Some(v) => Ok(Some(SessionRecord::deserialize(&B64.decode(v).map_err(|_| corrompido("sessão"))?)?)),
197
+ }
198
+ }
199
+
200
+ async fn store_session(&mut self, a: &ProtocolAddress, r: &SessionRecord) -> Res<()> {
201
+ self.sessoes.insert(chave_endereco(a), b64(&r.serialize()?));
202
+ Ok(())
203
+ }
204
+ }
205
+
206
+ // ------------------------------------------------------------ formatos do servidor
207
+
208
+ #[derive(Serialize, Deserialize)]
209
+ #[serde(rename_all = "camelCase")]
210
+ struct PreKeyDto {
211
+ key_id: u32,
212
+ public_key: String,
213
+ }
214
+
215
+ #[derive(Serialize, Deserialize)]
216
+ #[serde(rename_all = "camelCase")]
217
+ struct AssinadaDto {
218
+ key_id: u32,
219
+ public_key: String,
220
+ assinatura: String,
221
+ }
222
+
223
+ /// O `ChavesDoDispositivoDto` da API.
224
+ #[derive(Serialize)]
225
+ #[serde(rename_all = "camelCase")]
226
+ struct ChavesDto {
227
+ registration_id: u32,
228
+ identity_key: String,
229
+ signed_pre_key: AssinadaDto,
230
+ kyber_ultimo_recurso: AssinadaDto,
231
+ pre_keys: Vec<PreKeyDto>,
232
+ kyber_pre_keys: Vec<AssinadaDto>,
233
+ }
234
+
235
+ #[derive(Serialize)]
236
+ #[serde(rename_all = "camelCase")]
237
+ struct ReposicaoDto {
238
+ pre_keys: Vec<PreKeyDto>,
239
+ kyber_pre_keys: Vec<AssinadaDto>,
240
+ }
241
+
242
+ /// Um pacote de `GET /v1/chaves/:usuarioId`.
243
+ #[derive(Deserialize)]
244
+ #[serde(rename_all = "camelCase")]
245
+ struct PacoteDto {
246
+ dispositivo: u32,
247
+ registration_id: u32,
248
+ identity_key: String,
249
+ signed_pre_key: AssinadaDto,
250
+ pre_key: Option<PreKeyDto>,
251
+ kyber_pre_key: AssinadaDto,
252
+ }
253
+
254
+ #[derive(Serialize)]
255
+ #[serde(rename_all = "camelCase")]
256
+ struct Cifrado {
257
+ tipo: &'static str,
258
+ conteudo: String,
259
+ registration_id: u32,
260
+ }
261
+
262
+ const LOTE: usize = 50;
263
+ const LOTE_KYBER: usize = 20;
264
+ /// Ids de pré-chave cabem em 24 bits (limite do DTO do servidor).
265
+ const MAX_ID: u32 = 0xff_fff0;
266
+
267
+ fn proximo(contador: &mut u32) -> u32 {
268
+ if *contador == 0 || *contador >= MAX_ID {
269
+ *contador = 1 + rand::rng().random_range(0..0x7f_ffff);
270
+ }
271
+ let id = *contador;
272
+ *contador += 1;
273
+ id
274
+ }
275
+
276
+ #[wasm_bindgen]
277
+ pub struct Signal {
278
+ e: Estado,
279
+ }
280
+
281
+ impl Signal {
282
+ fn par(&self) -> Result<IdentityKeyPair, JsError> {
283
+ block_on(self.e.get_identity_key_pair()).map_err(erro)
284
+ }
285
+
286
+ fn local(&self) -> Result<ProtocolAddress, JsError> {
287
+ if self.e.usuario.is_empty() {
288
+ return Err(JsError::new("navegador ainda não vinculado"));
289
+ }
290
+ endereco(&self.e.usuario, self.e.dispositivo)
291
+ }
292
+
293
+ fn nova_signed(&mut self, ms: f64) -> Result<AssinadaDto, JsError> {
294
+ let par = self.par()?;
295
+ let id = proximo(&mut self.e.proximo_signed);
296
+ let mut rng = OsRng.unwrap_err();
297
+ let chaves = KeyPair::generate(&mut rng);
298
+ let assinatura = par.private_key().calculate_signature(&chaves.public_key.serialize(), &mut rng).map_err(erro)?;
299
+ let r = SignedPreKeyRecord::new(id.into(), Timestamp::from_epoch_millis(ms as u64), &chaves, &assinatura);
300
+ self.e.signed.insert(id, b64(&r.serialize().map_err(erro)?));
301
+ Ok(AssinadaDto { key_id: id, public_key: b64(&chaves.public_key.serialize()), assinatura: b64(&assinatura) })
302
+ }
303
+
304
+ fn nova_kyber(&mut self, ms: f64, ultimo_recurso: bool) -> Result<AssinadaDto, JsError> {
305
+ let par = self.par()?;
306
+ let id = proximo(&mut self.e.proximo_kyber);
307
+ let mut rng = OsRng.unwrap_err();
308
+ let chaves = kem::KeyPair::generate(kem::KeyType::Kyber1024, &mut rng);
309
+ let publica = chaves.public_key.serialize();
310
+ let assinatura = par.private_key().calculate_signature(&publica, &mut rng).map_err(erro)?;
311
+ let r = KyberPreKeyRecord::new(id.into(), Timestamp::from_epoch_millis(ms as u64), &chaves, &assinatura);
312
+ self.e.kyber.insert(id, Kyber { registro: b64(&r.serialize().map_err(erro)?), ultimo_recurso });
313
+ Ok(AssinadaDto { key_id: id, public_key: b64(&publica), assinatura: b64(&assinatura) })
314
+ }
315
+
316
+ fn novas_prekeys(&mut self, quantas: usize) -> Result<Vec<PreKeyDto>, JsError> {
317
+ let mut rng = OsRng.unwrap_err();
318
+ (0..quantas)
319
+ .map(|_| {
320
+ let id = proximo(&mut self.e.proximo_prekey);
321
+ let chaves = KeyPair::generate(&mut rng);
322
+ let r = PreKeyRecord::new(id.into(), &chaves);
323
+ self.e.prekeys.insert(id, b64(&r.serialize().map_err(erro)?));
324
+ Ok(PreKeyDto { key_id: id, public_key: b64(&chaves.public_key.serialize()) })
325
+ })
326
+ .collect()
327
+ }
328
+ }
329
+
330
+ #[wasm_bindgen]
331
+ impl Signal {
332
+ /// Identidade nova (navegador nunca vinculado).
333
+ #[wasm_bindgen(js_name = novo)]
334
+ pub fn novo() -> Signal {
335
+ let mut rng = OsRng.unwrap_err();
336
+ let par = IdentityKeyPair::generate(&mut rng);
337
+ let e = Estado {
338
+ identidade: b64(&par.serialize()),
339
+ // 14 bits, nunca zero (igual ao app).
340
+ registration_id: 1 + rng.random_range(0..16380),
341
+ ..Default::default()
342
+ };
343
+ Signal { e }
344
+ }
345
+
346
+ #[wasm_bindgen(js_name = carregar)]
347
+ pub fn carregar(json: &str) -> Result<Signal, JsError> {
348
+ Ok(Signal { e: serde_json::from_str(json).map_err(erro)? })
349
+ }
350
+
351
+ pub fn exportar(&self) -> Result<String, JsError> {
352
+ serde_json::to_string(&self.e).map_err(erro)
353
+ }
354
+
355
+ /// Pacote inicial (o `chaves` do pedido de vínculo), no formato da API.
356
+ #[wasm_bindgen(js_name = chavesIniciais)]
357
+ pub fn chaves_iniciais(&mut self, ms: f64) -> Result<String, JsError> {
358
+ let signed = self.nova_signed(ms)?;
359
+ let ultimo = self.nova_kyber(ms, true)?;
360
+ let pre_keys = self.novas_prekeys(LOTE)?;
361
+ let kyber_pre_keys = (0..LOTE_KYBER).map(|_| self.nova_kyber(ms, false)).collect::<Result<Vec<_>, _>>()?;
362
+ let par = self.par()?;
363
+ serde_json::to_string(&ChavesDto {
364
+ registration_id: self.e.registration_id,
365
+ identity_key: b64(&par.identity_key().serialize()),
366
+ signed_pre_key: signed,
367
+ kyber_ultimo_recurso: ultimo,
368
+ pre_keys,
369
+ kyber_pre_keys,
370
+ })
371
+ .map_err(erro)
372
+ }
373
+
374
+ /// Reposição de pré-chaves (`PUT /v1/chaves`).
375
+ #[wasm_bindgen(js_name = reposicao)]
376
+ pub fn reposicao(&mut self, ms: f64, pre_keys: usize, kyber: usize) -> Result<String, JsError> {
377
+ let pre_keys = self.novas_prekeys(pre_keys.min(100))?;
378
+ let kyber_pre_keys = (0..kyber.min(100)).map(|_| self.nova_kyber(ms, false)).collect::<Result<Vec<_>, _>>()?;
379
+ serde_json::to_string(&ReposicaoDto { pre_keys, kyber_pre_keys }).map_err(erro)
380
+ }
381
+
382
+ /// Chave pública de identidade (base64): vai no QR para o celular conferir.
383
+ #[wasm_bindgen(js_name = identidadePublica)]
384
+ pub fn identidade_publica(&self) -> Result<String, JsError> {
385
+ Ok(b64(&self.par()?.identity_key().serialize()))
386
+ }
387
+
388
+ #[wasm_bindgen(js_name = registrationId)]
389
+ pub fn registration_id(&self) -> u32 {
390
+ self.e.registration_id
391
+ }
392
+
393
+ /// Depois do vínculo: o endereço deste navegador.
394
+ #[wasm_bindgen(js_name = definirEndereco)]
395
+ pub fn definir_endereco(&mut self, usuario: &str, dispositivo: u32) -> Result<(), JsError> {
396
+ endereco(usuario, dispositivo)?;
397
+ self.e.usuario = usuario.to_owned();
398
+ self.e.dispositivo = dispositivo;
399
+ Ok(())
400
+ }
401
+
402
+ /// X3DH + Kyber (PQXDH): o libsignal confere as assinaturas e recusa pacote forjado.
403
+ #[wasm_bindgen(js_name = abrirSessao)]
404
+ pub fn abrir_sessao(&mut self, usuario: &str, pacote_json: &str, ms: f64) -> Result<(), JsError> {
405
+ let p: PacoteDto = serde_json::from_str(pacote_json).map_err(erro)?;
406
+ let device = DeviceId::try_from(p.dispositivo).map_err(|_| JsError::new("dispositivo inválido"))?;
407
+ let pre_key = match &p.pre_key {
408
+ None => None,
409
+ Some(k) => Some((PreKeyId::from(k.key_id), PublicKey::deserialize(&de_b64(&k.public_key)?).map_err(erro)?)),
410
+ };
411
+ let pacote = PreKeyBundle::new(
412
+ p.registration_id,
413
+ device,
414
+ pre_key,
415
+ p.signed_pre_key.key_id.into(),
416
+ PublicKey::deserialize(&de_b64(&p.signed_pre_key.public_key)?).map_err(erro)?,
417
+ de_b64(&p.signed_pre_key.assinatura)?,
418
+ p.kyber_pre_key.key_id.into(),
419
+ kem::PublicKey::deserialize(&de_b64(&p.kyber_pre_key.public_key)?).map_err(erro)?,
420
+ de_b64(&p.kyber_pre_key.assinatura)?,
421
+ IdentityKey::decode(&de_b64(&p.identity_key)?).map_err(erro)?,
422
+ )
423
+ .map_err(erro)?;
424
+ let remoto = endereco(usuario, p.dispositivo)?;
425
+ let local = self.local()?;
426
+ let mut rng = OsRng.unwrap_err();
427
+ // A mesma loja faz papel de sessões e identidades: cópia para o segundo `&mut`.
428
+ let mut ids = self.e.clone();
429
+ block_on(process_prekey_bundle(&remoto, &local, &mut self.e, &mut ids, &pacote, agora(ms), &mut rng)).map_err(erro)?;
430
+ self.e.identidades = ids.identidades;
431
+ self.e.trocas = ids.trocas;
432
+ Ok(())
433
+ }
434
+
435
+ /// Números dos aparelhos de alguém com quem há sessão.
436
+ #[wasm_bindgen(js_name = dispositivosComSessao)]
437
+ pub fn dispositivos_com_sessao(&self, usuario: &str) -> Vec<u32> {
438
+ let prefixo = format!("{usuario}.");
439
+ self.e
440
+ .sessoes
441
+ .keys()
442
+ .filter_map(|k| k.strip_prefix(&prefixo).and_then(|n| n.parse().ok()))
443
+ .collect()
444
+ }
445
+
446
+ #[wasm_bindgen(js_name = apagarSessao)]
447
+ pub fn apagar_sessao(&mut self, usuario: &str, dispositivo: u32) {
448
+ self.e.sessoes.remove(&format!("{usuario}.{dispositivo}"));
449
+ }
450
+
451
+ /// Cifra para um aparelho: `{tipo: PREKEY|WHISPER, conteudo, registrationId}`.
452
+ pub fn cifrar(&mut self, usuario: &str, dispositivo: u32, claro: &[u8], ms: f64) -> Result<String, JsError> {
453
+ let remoto = endereco(usuario, dispositivo)?;
454
+ let local = self.local()?;
455
+ let mut rng = OsRng.unwrap_err();
456
+ let mut ids = self.e.clone();
457
+ let msg = block_on(message_encrypt(claro, &remoto, &local, &mut self.e, &mut ids, agora(ms), &mut rng)).map_err(erro)?;
458
+ self.e.identidades = ids.identidades;
459
+ self.e.trocas = ids.trocas;
460
+ let registro = block_on(self.e.load_session(&remoto)).map_err(erro)?.ok_or_else(|| JsError::new("sem sessão"))?;
461
+ let tipo = match msg {
462
+ CiphertextMessage::PreKeySignalMessage(_) => "PREKEY",
463
+ _ => "WHISPER",
464
+ };
465
+ serde_json::to_string(&Cifrado {
466
+ tipo,
467
+ conteudo: b64(msg.serialize()),
468
+ registration_id: registro.remote_registration_id().map_err(erro)?,
469
+ })
470
+ .map_err(erro)
471
+ }
472
+
473
+ /// Decifra um envelope. Erro com "duplicate" no texto = mensagem repetida.
474
+ pub fn decifrar(&mut self, usuario: &str, dispositivo: u32, tipo: &str, conteudo_b64: &str) -> Result<Vec<u8>, JsError> {
475
+ let remoto = endereco(usuario, dispositivo)?;
476
+ let local = self.local()?;
477
+ let bytes = de_b64(conteudo_b64)?;
478
+ let mut rng = OsRng.unwrap_err();
479
+ // Loja de trabalho: só vale se a decifragem der certo (falha não estraga a sessão).
480
+ let mut trabalho = self.e.clone();
481
+ let mut ids = self.e.clone();
482
+ let claro = if tipo == "PREKEY" {
483
+ let m = PreKeySignalMessage::try_from(bytes.as_slice()).map_err(erro)?;
484
+ let mut pre = self.e.clone();
485
+ let mut kyber = self.e.clone();
486
+ let signed = self.e.clone();
487
+ let r = block_on(message_decrypt_prekey(
488
+ &m, &remoto, &local, &mut trabalho, &mut ids, &mut pre, &signed, &mut kyber, &mut rng,
489
+ ))
490
+ .map_err(erro)?;
491
+ trabalho.prekeys = pre.prekeys;
492
+ trabalho.kyber = kyber.kyber;
493
+ trabalho.kyber_vistos = kyber.kyber_vistos;
494
+ r
495
+ } else {
496
+ let m = SignalMessage::try_from(bytes.as_slice()).map_err(erro)?;
497
+ block_on(message_decrypt_signal(&m, &remoto, &local, &mut trabalho, &mut ids, &mut rng)).map_err(erro)?
498
+ };
499
+ trabalho.identidades = ids.identidades;
500
+ trabalho.trocas = ids.trocas;
501
+ self.e = trabalho;
502
+ Ok(claro)
503
+ }
504
+
505
+ /// Contatos que trocaram de identidade desde a última leitura.
506
+ #[wasm_bindgen(js_name = lerTrocas)]
507
+ pub fn ler_trocas(&mut self) -> Vec<String> {
508
+ std::mem::take(&mut self.e.trocas)
509
+ }
510
+ }
@@ -0,0 +1 @@
1
+ { "type": "commonjs" }
@@ -0,0 +1,52 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ export class Signal {
5
+ private constructor();
6
+ free(): void;
7
+ [Symbol.dispose](): void;
8
+ /**
9
+ * X3DH + Kyber (PQXDH): o libsignal confere as assinaturas e recusa pacote forjado.
10
+ */
11
+ abrirSessao(usuario: string, pacote_json: string, ms: number): void;
12
+ apagarSessao(usuario: string, dispositivo: number): void;
13
+ static carregar(json: string): Signal;
14
+ /**
15
+ * Pacote inicial (o `chaves` do pedido de vínculo), no formato da API.
16
+ */
17
+ chavesIniciais(ms: number): string;
18
+ /**
19
+ * Cifra para um aparelho: `{tipo: PREKEY|WHISPER, conteudo, registrationId}`.
20
+ */
21
+ cifrar(usuario: string, dispositivo: number, claro: Uint8Array, ms: number): string;
22
+ /**
23
+ * Decifra um envelope. Erro com "duplicate" no texto = mensagem repetida.
24
+ */
25
+ decifrar(usuario: string, dispositivo: number, tipo: string, conteudo_b64: string): Uint8Array;
26
+ /**
27
+ * Depois do vínculo: o endereço deste navegador.
28
+ */
29
+ definirEndereco(usuario: string, dispositivo: number): void;
30
+ /**
31
+ * Números dos aparelhos de alguém com quem há sessão.
32
+ */
33
+ dispositivosComSessao(usuario: string): Uint32Array;
34
+ exportar(): string;
35
+ /**
36
+ * Chave pública de identidade (base64): vai no QR para o celular conferir.
37
+ */
38
+ identidadePublica(): string;
39
+ /**
40
+ * Contatos que trocaram de identidade desde a última leitura.
41
+ */
42
+ lerTrocas(): string[];
43
+ /**
44
+ * Identidade nova (navegador nunca vinculado).
45
+ */
46
+ static novo(): Signal;
47
+ registrationId(): number;
48
+ /**
49
+ * Reposição de pré-chaves (`PUT /v1/chaves`).
50
+ */
51
+ reposicao(ms: number, pre_keys: number, kyber: number): string;
52
+ }