Compare commits

..

28 Commits

Author SHA1 Message Date
moiseev-signal
7c8cb0c5fc
keytrans: Add reset account data field function
Some checks failed
[CI] Check Versions / Check version number consistency (push) Has been cancelled
2026-05-16 12:07:54 -07:00
moiseev-signal
c41e917d4e
keytrans: Log data versions before and after monitor 2026-05-15 16:33:05 -07:00
marc-signal
4d43a6270a
Use linkme, not macro expansion, for Native.ts generation 2026-05-15 14:16:26 -04:00
andrew-signal
7543c3d35b
net: partially implement reflector proxy type 2026-05-14 16:53:47 -04:00
Jordan Rose
bd383e51f0 testing: Add a timeout for FakeChatRemote receive operations
These shouldn't come up in normal use; they only stop tests from
waiting indefinitely when the test author has made a mistake.
2026-05-14 11:30:51 -07:00
Jordan Rose
fb9407cbcb chat: Expose a raw_grpc endpoint 2026-05-14 10:57:50 -07:00
Jordan Rose
9903175a51 Expose gRPC+JSON testing to Swift 2026-05-14 09:49:36 -07:00
Jordan Rose
af55da7bbd bridge: Expose FakeChatRemote gRPC testing endpoints to apps 2026-05-14 09:49:36 -07:00
Jordan Rose
875f93019b Add JSON conversion and gRPC framing APIs for testing 2026-05-14 09:49:36 -07:00
Jordan Rose
73bcc78e12 net: Add an H2/gRPC connection to FakeChatRemote 2026-05-14 09:49:36 -07:00
Jordan Rose
d0b3edc0f1 Add grpcOverrides to FakeChatRemote
Unused until we have H2 support as well (coming soon!)
2026-05-14 09:49:36 -07:00
Jordan Rose
ec67c55017 taplo: Don't try to vertically align comments in toml files 2026-05-14 09:49:36 -07:00
Jordan Rose
ee47959258 Add grpc.BackupsAnonymousGetUploadForm remote config 2026-05-14 09:47:19 -07:00
moiseev-signal
7b399f26d8
Update mac setup script 2026-05-13 15:10:27 -07:00
Jordan Rose
f70d1faaa0 grpc: Drop idea of "JSON codec", just provide binproto<->JSON by name
Besides being simpler, it also makes it easier to avoid additional
code size costs in non-testing builds (by simply not referencing these
functions).
2026-05-13 14:54:07 -07:00
Jordan Rose
b8f2aaf5dc java: Silence unchecked cast in UploadForm.fromNative
...and make it private.
2026-05-12 15:59:50 -07:00
Max Moiseev
2af375875b Reset for version v0.94.1 2026-05-08 16:26:57 -07:00
moiseev-signal
6e5a0466b3
Remove unused SignalMessage.verifyMac 2026-05-08 15:18:31 -07:00
gram-signal
2486ffe4e2
SPQR: API changes to allow remote config for archiving non-PQ sessions. 2026-05-08 14:06:24 -07:00
moiseev-signal
9adf4191f0
keytrans: Detect version changes sooner 2026-05-07 16:34:04 -07:00
Jordan Rose
4fe3cbf6b6 chat: Tidy up gRPC test cases
- Prefer GrpcOverrideRequestValidator when a gRPC override is
  implemented (at the moment, only the backup get_upload_form and
  get_media_upload_form methods do not have overrides)
- Add UnreachableValidator for checking that a request is never sent
  in the first place
- Better doc comments for the various Validators
- Bring account existence tests closer to the other tests
2026-05-07 12:13:21 -07:00
Jordan Rose
a360c14a58 Fix -Zdirect-minimal-versions build
- libc needed an update after the tokio update
- older scopeguard::defer! only takes an expression
2026-05-07 10:14:38 -07:00
marc-signal
d33445757d
Upgrade tokio to 1.52 2026-05-06 18:53:33 -04:00
Jordan Rose
2c7c0d16dc net: Test that H2 GOAWAY disconnects a chat websocket immediately 2026-05-06 15:12:00 -07:00
Jordan Rose
9be982cbf3
libsignal-net-grpc: add pbjson mode
Adds a mode where gRPC requests are encoded as protobuf JSON (using 
pbjson + serde_json) instead of binary protobuf (using prost). Because
tonic-build hardcodes that the codec initialization is done via
default(), the choice is done based on whether the current tokio
runtime's Id is in a "JSON mode" set; this is not perfect because
tokio will reuse Ids across runtimes, but we're only planning to use
this for testing anyway.
2026-05-06 11:35:09 -07:00
Jordan Rose
8b4eff395e chat: Consistently check BackupsAnonymousGetUploadForm for gRPC
There's still no remote config for this setting, but when we're ready
for one it will behave as expected.
2026-05-06 11:10:40 -07:00
Jordan Rose
a84b3560d1
bridge: Expose SyncInputStream to FFI with bridge_callbacks directly
...instead of using a wrapper FFI-only InputStream trait. We still do
that for the JNI and Node bridges because the shape doesn't match up,
but even there we can use a blanket impl to simplify the JNI bridge a
little.
2026-05-06 10:30:12 -07:00
Andrew
36add9ba9b Reset for version v0.93.3 2026-05-05 21:02:21 -04:00
122 changed files with 5278 additions and 2911 deletions

View File

@ -589,7 +589,7 @@ jobs:
node-version-file: '.nvmrc'
- name: Verify that the Node bindings are up to date
run: rust/bridge/node/bin/gen_ts_decl.py --verify
run: cargo run -p libsignal-node-native_ts -- --verify
if: startsWith(matrix.os, 'ubuntu-')
- run: npm ci

View File

@ -154,7 +154,7 @@ jobs:
- run: sudo apt-get install -U protobuf-compiler
- name: Verify that the Node bindings are up to date
run: rust/bridge/node/bin/gen_ts_decl.py --verify
run: cargo run -p libsignal-node-native_ts -- --verify
publish:
name: Publish

View File

@ -1,13 +1,14 @@
include = ["Cargo.toml", "rust/**/*.toml"]
[formatting]
reorder_keys = false
align_comments = false
indent_string = ' '
reorder_keys = false
[[rule]]
include = ["**/Cargo.toml"]
keys = ["dependencies", "workspace.dependencies", "dev-dependencies", "build-dependencies"]
[rule.formatting]
reorder_keys = true
inline_table_expand = false
reorder_keys = true

150
Cargo.lock generated
View File

@ -2265,9 +2265,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.180"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libcrux-hacl-rs"
@ -2494,6 +2494,7 @@ dependencies = [
"libsignal-message-backup",
"libsignal-net",
"libsignal-net-chat",
"libsignal-net-grpc",
"libsignal-protocol",
"linkme",
"neon",
@ -2608,14 +2609,14 @@ dependencies = [
[[package]]
name = "libsignal-debug"
version = "0.93.2"
version = "0.94.1"
dependencies = [
"cfg-if",
]
[[package]]
name = "libsignal-ffi"
version = "0.93.2"
version = "0.94.1"
dependencies = [
"cpufeatures 0.2.17",
"hex",
@ -2636,7 +2637,7 @@ dependencies = [
[[package]]
name = "libsignal-jni"
version = "0.93.2"
version = "0.94.1"
dependencies = [
"libsignal-debug",
"libsignal-jni-impl",
@ -2644,7 +2645,7 @@ dependencies = [
[[package]]
name = "libsignal-jni-impl"
version = "0.93.2"
version = "0.94.1"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
@ -2661,7 +2662,7 @@ dependencies = [
[[package]]
name = "libsignal-jni-testing"
version = "0.93.2"
version = "0.94.1"
dependencies = [
"jni 0.21.1",
"libsignal-bridge-testing",
@ -2795,6 +2796,7 @@ dependencies = [
"hkdf",
"hmac",
"http",
"http-body-util",
"hyper",
"hyper-util",
"itertools 0.14.0",
@ -2880,6 +2882,7 @@ dependencies = [
"rand 0.9.4",
"rand_chacha",
"ref-cast",
"scopeguard",
"serde",
"serde_json",
"serde_with",
@ -2905,9 +2908,15 @@ name = "libsignal-net-grpc"
version = "0.1.0"
dependencies = [
"const-str",
"derive-where",
"libsignal-core",
"pbjson",
"pbjson-build",
"pbjson-types",
"prost",
"prost-types",
"serde",
"serde_json",
"strum",
"tonic",
"tonic-prost",
@ -2977,12 +2986,13 @@ dependencies = [
[[package]]
name = "libsignal-node"
version = "0.93.2"
version = "0.94.1"
dependencies = [
"futures",
"libsignal-bridge",
"libsignal-bridge-macros",
"libsignal-bridge-testing",
"libsignal-bridge-types",
"libsignal-protocol",
"linkme",
"log",
@ -2996,6 +3006,19 @@ dependencies = [
"uuid",
]
[[package]]
name = "libsignal-node-native_ts"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"libsignal-bridge",
"libsignal-bridge-testing",
"libsignal-bridge-types",
"libsignal-node",
"minijinja",
]
[[package]]
name = "libsignal-protocol"
version = "0.1.0"
@ -3197,6 +3220,12 @@ dependencies = [
"libc",
]
[[package]]
name = "memo-map"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
[[package]]
name = "mime"
version = "0.3.17"
@ -3282,6 +3311,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "minijinja"
version = "2.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "805bfd7352166bae857ee569628b52bcd85a1cecf7810861ebceb1686b72b75d"
dependencies = [
"indexmap 2.13.0",
"memo-map",
"serde",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -3300,9 +3340,9 @@ dependencies = [
[[package]]
name = "mio"
version = "1.1.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi",
@ -4882,12 +4922,12 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@ -5261,9 +5301,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.49.0"
version = "1.52.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
dependencies = [
"bytes",
"libc",
@ -5286,9 +5326,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
@ -6050,15 +6090,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@ -6092,30 +6123,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
@ -6128,12 +6142,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@ -6146,12 +6154,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@ -6164,24 +6166,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@ -6194,12 +6184,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@ -6212,12 +6196,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@ -6230,12 +6208,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@ -6248,12 +6220,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"

View File

@ -22,6 +22,7 @@ members = [
"rust/bridge/jni/impl",
"rust/bridge/jni/testing",
"rust/bridge/node",
"rust/bridge/node/native_ts",
]
default-members = [
"rust/crypto",
@ -38,7 +39,7 @@ default-members = [
resolver = "2" # so that our dev-dependency features don't leak into products
[workspace.package]
version = "0.93.2"
version = "0.94.1"
authors = ["Signal Messenger LLC"]
license = "AGPL-3.0-only"
rust-version = "1.88"
@ -67,6 +68,7 @@ libsignal-message-backup = { path = "rust/message-backup" }
libsignal-net = { path = "rust/net" }
libsignal-net-chat = { path = "rust/net/chat" }
libsignal-net-grpc = { path = "rust/net/grpc" }
libsignal-node = { path = "rust/bridge/node" }
libsignal-protocol = { path = "rust/protocol" }
libsignal-svrb = { path = "rust/svrb" }
poksho = { path = "rust/poksho" }
@ -150,7 +152,7 @@ indexmap = "2.7.0"
intmap = "3.1.2"
itertools = "0.14.0"
jni = "0.21"
libc = "0.2.175"
libc = "0.2.186"
libcrux-ml-kem = { version = "0.0.8", default-features = false }
linkme = "0.3.33"
log = "0.4.21"
@ -160,6 +162,7 @@ mediasan-common = "0.5.3"
minidump = { version = "0.22.1", default-features = false }
minidump-processor = { version = "0.22.1", default-features = false }
minidump-unwind = { version = "0.22.1", default-features = false }
minijinja = "2.19.0"
mp4san = "0.5.3"
neon = { version = "1.1.0", default-features = false }
nonzero_ext = "0.3.0"
@ -209,7 +212,7 @@ test-log = "0.2.16"
testing_logger = "0.1.1"
thiserror = "2.0.11"
tls-parser = "0.12.2"
tokio = "1.45"
tokio = "1.52.2"
tokio-socks = "0.5.2"
tokio-stream = "0.1.16"
tokio-tungstenite = "0.28.0"

View File

@ -5,7 +5,7 @@
Pod::Spec.new do |s|
s.name = 'LibSignalClient'
s.version = '0.93.2'
s.version = '0.94.1'
s.summary = 'A Swift wrapper library for communicating with the Signal messaging service.'
s.homepage = 'https://github.com/signalapp/libsignal'

View File

@ -214,7 +214,7 @@ $ npm run test
When testing changes locally, you can use `npm run build` to do an incremental rebuild of the Rust library. Alternately, `npm run build-with-debug-level-logs` will rebuild without filtering out debug- and verbose-level logs.
When exposing new APIs to Node, you will need to run `rust/bridge/node/bin/gen_ts_decl.py` in
When exposing new APIs to Node, you will need to run `just generate-node` in
addition to rebuilding.
[nvm]: https://github.com/nvm-sh/nvm

View File

@ -1,6 +1,5 @@
v0.93.2
v0.94.1
- CDSI: production enclave switch to 15637fa1
- SVR: New enclaves for 2026Q2 for staging, and configurations (but not use) for prod.
- node: Expose SVR2-related functionality
- node: Support non-ASCII usernames and passwords in proxy URLs
- Add `grpc.BackupsAnonymousGetUploadForm` remote config, for both backup and backup media uploads. This is separate from the `grpc.AttachmentsGetUploadForm` config added previously, which applies to regular attachment uploads.
- keytrans: Add reset account data field functionality for all platforms.

View File

@ -3329,7 +3329,7 @@ DEALINGS IN THE SOFTWARE.
```
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, pkg-config 0.3.32, rustc-demangle 0.1.26, socket2 0.6.1
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, pkg-config 0.3.32, rustc-demangle 0.1.26, socket2 0.6.3
```
Copyright (c) 2014 Alex Crichton
@ -3424,7 +3424,7 @@ DEALINGS IN THE SOFTWARE.
```
## mio 1.1.1
## mio 1.2.0
```
Copyright (c) 2014 Carl Lerche and other MIO contributors
@ -3601,37 +3601,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## libc 0.2.180
```
Copyright (c) 2014-2020 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
## flate2 1.1.5
```
@ -4151,6 +4120,31 @@ DEALINGS IN THE SOFTWARE.
```
## httpdate 1.0.3
```
Copyright (c) 2016 Pyfisch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
## rustc_version 0.4.1
```
@ -5952,6 +5946,37 @@ SOFTWARE.
```
## libc 0.2.186
```
Copyright (c) The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
## arrayvec 0.7.6
```
@ -6276,7 +6301,7 @@ SOFTWARE.
```
## tokio-macros 2.6.0
## tokio-macros 2.7.0
```
MIT License
@ -6544,7 +6569,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.49.0
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.52.2
```
MIT License

View File

@ -3329,7 +3329,7 @@ DEALINGS IN THE SOFTWARE.
```
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, pkg-config 0.3.32, rustc-demangle 0.1.26, socket2 0.6.1
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, pkg-config 0.3.32, rustc-demangle 0.1.26, socket2 0.6.3
```
Copyright (c) 2014 Alex Crichton
@ -3424,7 +3424,7 @@ DEALINGS IN THE SOFTWARE.
```
## mio 1.1.1
## mio 1.2.0
```
Copyright (c) 2014 Carl Lerche and other MIO contributors
@ -3601,37 +3601,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## libc 0.2.180
```
Copyright (c) 2014-2020 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
## flate2 1.1.5
```
@ -4151,6 +4120,31 @@ DEALINGS IN THE SOFTWARE.
```
## httpdate 1.0.3
```
Copyright (c) 2016 Pyfisch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
## rustc_version 0.4.1
```
@ -5952,6 +5946,37 @@ SOFTWARE.
```
## libc 0.2.186
```
Copyright (c) The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
## arrayvec 0.7.6
```
@ -6276,7 +6301,7 @@ SOFTWARE.
```
## tokio-macros 2.6.0
## tokio-macros 2.7.0
```
MIT License
@ -6544,7 +6569,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.49.0
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.52.2
```
MIT License

View File

@ -3209,7 +3209,7 @@ third-party/chromium/LICENSE.
```
## windows-core 0.62.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.52.6, windows-targets 0.53.5, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1
## windows-core 0.62.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.61.2, windows-targets 0.52.6, windows_aarch64_msvc 0.52.6, windows_x86_64_gnu 0.52.6, windows_x86_64_msvc 0.52.6
```
MIT License
@ -3419,7 +3419,7 @@ DEALINGS IN THE SOFTWARE.
```
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, openssl-probe 0.2.0, rustc-demangle 0.1.26, socket2 0.6.1
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, openssl-probe 0.2.0, rustc-demangle 0.1.26, socket2 0.6.3
```
Copyright (c) 2014 Alex Crichton
@ -3482,7 +3482,7 @@ DEALINGS IN THE SOFTWARE.
```
## mio 1.1.1
## mio 1.2.0
```
Copyright (c) 2014 Carl Lerche and other MIO contributors
@ -3659,37 +3659,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## libc 0.2.180
```
Copyright (c) 2014-2020 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
## flate2 1.1.5
```
@ -4272,6 +4241,31 @@ DEALINGS IN THE SOFTWARE.
```
## httpdate 1.0.3
```
Copyright (c) 2016 Pyfisch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
## rustc_version 0.4.1
```
@ -6199,6 +6193,37 @@ SOFTWARE.
```
## libc 0.2.186
```
Copyright (c) The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
## arrayvec 0.7.6
```
@ -6635,7 +6660,7 @@ SOFTWARE.
```
## tokio-macros 2.6.0
## tokio-macros 2.7.0
```
MIT License
@ -6930,7 +6955,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.49.0
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.52.2
```
MIT License

View File

@ -3517,7 +3517,7 @@ DEALINGS IN THE SOFTWARE.
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, rustc-demangle 0.1.26, socket2 0.6.1</string>
<string>backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, rustc-demangle 0.1.26, socket2 0.6.3</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
@ -3582,7 +3582,7 @@ THE SOFTWARE.
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>mio 1.1.1</string>
<string>mio 1.2.0</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
@ -3758,41 +3758,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2014-2020 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the &quot;Software&quot;), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</string>
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>libc 0.2.180</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2014-2025 Alex Crichton
@ -4355,6 +4320,35 @@ DEALINGS IN THE SOFTWARE.
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2016 Pyfisch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the &quot;Software&quot;), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
</string>
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>httpdate 1.0.3</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2016 The Rust Project Developers
@ -6356,6 +6350,41 @@ SOFTWARE.
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the &quot;Software&quot;), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</string>
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>libc 0.2.186</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) Ulrik Sverdrup &quot;bluss&quot; 2015-2023
@ -6756,7 +6785,7 @@ SOFTWARE.
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>tokio-macros 2.6.0</string>
<string>tokio-macros 2.7.0</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
@ -7063,7 +7092,7 @@ SOFTWARE.
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.49.0</string>
<string>tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.52.2</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>

View File

@ -46,9 +46,9 @@
<h2>Overview of licenses:</h2>
<ul class="licenses-overview">
<li><a href="#MIT">MIT License</a> (354)</li>
<li><a href="#AGPL-3.0-only">GNU Affero General Public License v3.0 only</a> (36)</li>
<li><a href="#Apache-2.0">Apache License 2.0</a> (25)</li>
<li><a href="#MIT">MIT License</a> (349)</li>
<li><a href="#AGPL-3.0-only">GNU Affero General Public License v3.0 only</a> (37)</li>
<li><a href="#Apache-2.0">Apache License 2.0</a> (27)</li>
<li><a href="#BSD-3-Clause">BSD 3-Clause &quot;New&quot; or &quot;Revised&quot; License</a> (9)</li>
<li><a href="#ISC">ISC License</a> (4)</li>
<li><a href="#MPL-2.0">Mozilla Public License 2.0</a> (2)</li>
@ -741,6 +741,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see
<li><a href="https://crates.io/crates/libsignal-node">libsignal-node</a></li>
<li><a href="https://crates.io/crates/signal-neon-futures">signal-neon-futures</a></li>
<li><a href="https://crates.io/crates/signal-neon-futures-tests">signal-neon-futures-tests</a></li>
<li><a href="https://crates.io/crates/libsignal-node-native_ts">libsignal-node-native_ts</a></li>
<li><a href="https://crates.io/crates/libsignal-bridge">libsignal-bridge</a></li>
<li><a href="https://crates.io/crates/libsignal-bridge-macros">libsignal-bridge-macros</a></li>
<li><a href="https://crates.io/crates/libsignal-bridge-testing">libsignal-bridge-testing</a></li>
@ -2937,6 +2938,8 @@ END OF TERMS AND CONDITIONS
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href="https://github.com/getsentry/rust-debugid">debugid 0.8.0</a></li>
<li><a href="https://github.com/mitsuhiko/memo-map">memo-map 0.3.3</a></li>
<li><a href="https://github.com/mitsuhiko/minijinja">minijinja 2.19.0</a></li>
<li><a href="https://github.com/tokio-rs/prost">prost-build 0.14.1</a></li>
<li><a href="https://github.com/tokio-rs/prost">prost-derive 0.14.1</a></li>
<li><a href="https://github.com/tokio-rs/prost">prost-types 0.14.1</a></li>
@ -3558,19 +3561,14 @@ third-party/chromium/LICENSE.
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.45.0</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.52.0</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.59.0</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.60.2</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.61.2</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows-targets 0.42.2</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows-targets 0.52.6</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows-targets 0.53.5</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows_aarch64_msvc 0.42.2</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows_aarch64_msvc 0.52.6</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows_aarch64_msvc 0.53.1</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_gnu 0.52.6</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_gnu 0.53.1</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_msvc 0.42.2</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_msvc 0.52.6</a></li>
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_msvc 0.53.1</a></li>
</ul>
<pre class="license-text"> MIT License
@ -3839,7 +3837,7 @@ DEALINGS IN THE SOFTWARE.
<li><a href="https://github.com/rust-lang/pkg-config-rs">pkg-config 0.3.32</a></li>
<li><a href="https://github.com/rust-lang/rustc-demangle">rustc-demangle 0.1.26</a></li>
<li><a href="https://github.com/alexcrichton/scoped-tls">scoped-tls 1.0.1</a></li>
<li><a href="https://github.com/rust-lang/socket2">socket2 0.6.1</a></li>
<li><a href="https://github.com/rust-lang/socket2">socket2 0.6.3</a></li>
</ul>
<pre class="license-text">Copyright (c) 2014 Alex Crichton
@ -3940,7 +3938,7 @@ DEALINGS IN THE SOFTWARE.
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href="https://github.com/tokio-rs/mio">mio 1.1.1</a></li>
<li><a href="https://github.com/tokio-rs/mio">mio 1.2.0</a></li>
</ul>
<pre class="license-text">Copyright (c) 2014 Carl Lerche and other MIO contributors
@ -4160,39 +4158,6 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</pre>
</li>
<li class="license">
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href="https://github.com/rust-lang/libc">libc 0.2.180</a></li>
</ul>
<pre class="license-text">Copyright (c) 2014-2020 The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the &quot;Software&quot;), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</pre>
</li>
<li class="license">
@ -7207,6 +7172,39 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</pre>
</li>
<li class="license">
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href="https://github.com/rust-lang/libc">libc 0.2.186</a></li>
</ul>
<pre class="license-text">Copyright (c) The Rust Project Developers
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the &quot;Software&quot;), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</pre>
</li>
<li class="license">
<h3 id="MIT">MIT License</h3>
@ -7731,7 +7729,7 @@ SOFTWARE.
<h3 id="MIT">MIT License</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
<li><a href="https://github.com/tokio-rs/tokio">tokio-macros 2.6.0</a></li>
<li><a href="https://github.com/tokio-rs/tokio">tokio-macros 2.7.0</a></li>
</ul>
<pre class="license-text">MIT License
@ -8055,7 +8053,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
<ul class="license-used-by">
<li><a href="https://github.com/tokio-rs/tokio">tokio-stream 0.1.18</a></li>
<li><a href="https://github.com/tokio-rs/tokio">tokio-util 0.7.18</a></li>
<li><a href="https://github.com/tokio-rs/tokio">tokio 1.49.0</a></li>
<li><a href="https://github.com/tokio-rs/tokio">tokio 1.52.2</a></li>
</ul>
<pre class="license-text">MIT License

View File

@ -21,15 +21,14 @@ brew "rustup"
brew "shellcheck"
brew "swiftlint"
brew "taplo"
brew "terraform"
brew "yamllint"
cask "google-cloud-sdk"
cask "gcloud-cli"
EOF
# Install Python tools using pipx.
# This keeps their dependencies isolated from other things on your system,
# but is still global state for each tool. We may some day want to switch this to a venv instead.
"$(brew --prefix pipx)/bin/pipx" install mypy
"$(brew --prefix pipx)/bin/pipx" install "mypy<2.0"
"$(brew --prefix pipx)/bin/pipx" install flake8
"$(brew --prefix pipx)/bin/pipx" inject flake8 \
flake8-comprehensions \

View File

@ -23,7 +23,7 @@ repositories {
}
allprojects {
version = "0.93.2"
version = "0.94.1"
group = "org.signal"
tasks.withType(JavaCompile) {

View File

@ -63,7 +63,7 @@ public class AuthenticatedChatConnection extends ChatConnection {
*/
public static Pair<AuthenticatedChatConnection, FakeChatRemote> fakeConnect(
final TokioAsyncContext tokioAsyncContext, ChatConnectionListener listener) {
return fakeConnect(tokioAsyncContext, listener, new String[0]);
return fakeConnect(tokioAsyncContext, listener, new String[0], new String[0]);
}
/**
@ -72,14 +72,20 @@ public class AuthenticatedChatConnection extends ChatConnection {
* <p>The returned {@link FakeChatRemote} can be used to send messages to the connection.
*/
public static Pair<AuthenticatedChatConnection, FakeChatRemote> fakeConnect(
final TokioAsyncContext tokioAsyncContext, ChatConnectionListener listener, String[] alerts) {
final TokioAsyncContext tokioAsyncContext,
ChatConnectionListener listener,
String[] grpcOverrides,
String[] alerts) {
return tokioAsyncContext.guardedMap(
asyncContextHandle -> {
SetChatLaterListenerBridge bridgeListener = new SetChatLaterListenerBridge();
long fakeChatConnection =
NativeTesting.TESTING_FakeChatConnection_Create(
asyncContextHandle, bridgeListener, String.join("\n", alerts));
asyncContextHandle,
bridgeListener,
String.join("\n", grpcOverrides),
String.join("\n", alerts));
AuthenticatedChatConnection chat =
new AuthenticatedChatConnection(
tokioAsyncContext,

View File

@ -4,7 +4,11 @@
//
package org.signal.libsignal.net
public abstract class KeyTransparency {
import org.signal.libsignal.internal.Native
import org.signal.libsignal.keytrans.Store
import org.signal.libsignal.protocol.ServiceId
public object KeyTransparency {
/**
* Mode of the key transparency operation.
*
@ -32,4 +36,44 @@ public abstract class KeyTransparency {
public fun isSelf(): Boolean = this is Self
}
/**
* A tag identifying an optional field of the account data.
*
* (Must be in sync with the Rust counterpart)
*/
public enum class AccountDataField(
public val value: Int,
) {
E164(0),
USERNAME_HASH(1),
}
/**
* Resets a particular field in the data associated with given ACI.
*
* Must only be called for the "self" account when either E.164 or username change is performed.
*
* Upon successful completion the data associated with the account will be updated in the store, if it
* was present to begin with, noop if it was not.
*
* @param aci An ACI of "self" account.
* @param field Account data field to be reset (E.164 or username hash)
* @param store local persistent storage for key transparency-related data.
* @throws IllegalArgumentException if the stored data cannot be decoded correctly, which means data corruption.
*/
@JvmStatic
public fun resetField(
aci: ServiceId.Aci,
field: AccountDataField,
store: Store,
) {
store.getAccountData(aci).map {
val updated = Native.KeyTransparency_ResetDataField(it, field.value)
if (updated.isEmpty()) {
throw IllegalArgumentException("failed to decode account data")
}
store.setAccountData(aci, updated)
}
}
}

View File

@ -77,13 +77,26 @@ public class UnauthenticatedChatConnection extends ChatConnection {
final TokioAsyncContext tokioAsyncContext,
ChatConnectionListener listener,
Network.Environment ktEnvironment) {
return fakeConnect(tokioAsyncContext, listener, new String[0], ktEnvironment);
}
/**
* Test-only method to create a {@code UnauthenticatedChatConnection} connected to a fake remote.
*
* <p>The returned {@link FakeChatRemote} can be used to send messages to the connection.
*/
public static Pair<UnauthenticatedChatConnection, FakeChatRemote> fakeConnect(
final TokioAsyncContext tokioAsyncContext,
ChatConnectionListener listener,
String[] grpcOverrides,
Network.Environment ktEnvironment) {
return tokioAsyncContext.guardedMap(
asyncContextHandle -> {
SetChatLaterListenerBridge bridgeListener = new SetChatLaterListenerBridge();
long fakeChatConnection =
NativeTesting.TESTING_FakeChatConnection_Create(
asyncContextHandle, bridgeListener, "");
asyncContextHandle, bridgeListener, String.join("\n", grpcOverrides), "");
UnauthenticatedChatConnection chat =
new UnauthenticatedChatConnection(
tokioAsyncContext,

View File

@ -18,7 +18,8 @@ public data class UploadForm(
public companion object {
@JvmStatic
@CalledFromNative
public fun fromNative(
@Suppress("UNCHECKED_CAST")
private fun fromNative(
cdn: Int,
key: String,
headers: Array<*>,

View File

@ -169,7 +169,6 @@ class AuthMessagesServiceTest {
AuthenticatedChatConnection.fakeConnect(
tokioAsyncContext,
NoOpListener(),
emptyArray(),
)
listOf(
@ -199,7 +198,6 @@ class AuthMessagesServiceTest {
AuthenticatedChatConnection.fakeConnect(
tokioAsyncContext,
NoOpListener(),
emptyArray(),
)
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
@ -224,7 +222,6 @@ class AuthMessagesServiceTest {
AuthenticatedChatConnection.fakeConnect(
tokioAsyncContext,
NoOpListener(),
emptyArray(),
)
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
@ -268,7 +265,6 @@ class AuthMessagesServiceTest {
AuthenticatedChatConnection.fakeConnect(
tokioAsyncContext,
NoOpListener(),
emptyArray(),
)
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
@ -310,7 +306,6 @@ class AuthMessagesServiceTest {
AuthenticatedChatConnection.fakeConnect(
tokioAsyncContext,
NoOpListener(),
emptyArray(),
)
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)

View File

@ -263,7 +263,7 @@ public class ChatServiceTest {
final Listener listener = new Listener();
final Pair<AuthenticatedChatConnection, FakeChatRemote> chatAndFakeRemote =
AuthenticatedChatConnection.fakeConnect(
tokioAsyncContext, listener, new String[] {"UPPERcase", "lowercase"});
tokioAsyncContext, listener, new String[0], new String[] {"UPPERcase", "lowercase"});
final AuthenticatedChatConnection chat = chatAndFakeRemote.getFirst();
final FakeChatRemote fakeRemote = chatAndFakeRemote.getSecond();

View File

@ -11,6 +11,7 @@ import java.util.UUID;
import org.junit.Test;
import org.signal.libsignal.internal.NativeTesting;
import org.signal.libsignal.keytrans.KeyTransparencyException;
import org.signal.libsignal.keytrans.TestStore;
import org.signal.libsignal.keytrans.VerificationFailedException;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
@ -60,4 +61,29 @@ public class KeyTransparencyTest {
public void canBridgeChatSendError() {
assertThrows(TimeoutException.class, NativeTesting::TESTING_KeyTransChatSendError);
}
@Test
public void resetFieldThrowsOnCorruptData() {
var store = new TestStore();
store.setAccountData(TEST_ACI, new byte[] {1, 2, 3});
assertThrows(
IllegalArgumentException.class,
() -> KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store));
}
@Test
public void resetFieldIsNoopWhenDataIsMissing() {
var store = new TestStore();
KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store);
assert (store.storage.get(TEST_ACI).isEmpty());
}
@Test
public void resetFieldUpdatesStoreOnSuccess() {
var store = new TestStore();
store.setAccountData(TEST_ACI, NativeTesting.TESTING_KeyTransStoredAccountData());
assertEquals(1, store.storage.get(TEST_ACI).size());
KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store);
assertEquals(2, store.storage.get(TEST_ACI).size());
}
}

View File

@ -300,8 +300,8 @@ public class SessionBuilderTest {
aliceSessionBuilder.process(bobPreKey, Instant.EPOCH);
SessionRecord initialSession = aliceStore.loadSession(BOB_ADDRESS);
assertTrue(initialSession.hasSenderChain(Instant.EPOCH));
assertFalse(initialSession.hasSenderChain(Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
assertTrue(initialSession.hasSenderChain(1.0, Instant.EPOCH));
assertFalse(initialSession.hasSenderChain(1.0, Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
String originalMessage = "Good, fast, cheap: pick two";
SessionCipher aliceSessionCipher = new SessionCipher(aliceStore, ALICE_ADDRESS, BOB_ADDRESS);
@ -311,8 +311,8 @@ public class SessionBuilderTest {
assertTrue(outgoingMessage.getType() == CiphertextMessage.PREKEY_TYPE);
SessionRecord updatedSession = aliceStore.loadSession(BOB_ADDRESS);
assertTrue(updatedSession.hasSenderChain(Instant.EPOCH));
assertFalse(updatedSession.hasSenderChain(Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
assertTrue(updatedSession.hasSenderChain(1.0, Instant.EPOCH));
assertFalse(updatedSession.hasSenderChain(1.0, Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
try {
aliceSessionCipher.encrypt(

View File

@ -8,8 +8,10 @@ package org.signal.libsignal.protocol;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import java.time.Instant;
import org.junit.Test;
import org.signal.libsignal.internal.NativeTesting;
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
@ -29,7 +31,7 @@ public class SessionRecordTest {
public void testUninitAccess() {
SessionRecord empty_record = new SessionRecord();
assertFalse(empty_record.hasSenderChain());
assertFalse(empty_record.hasSenderChain(1.0));
assertEquals(empty_record.getSessionVersion(), 0);
}
@ -76,4 +78,25 @@ public class SessionRecordTest {
assertThrows(InvalidKeyException.class, () -> record.getKeyPair());
}
}
@Test
public void testHasUsablePQRatio() throws Exception {
// Record with key "\x7f\x7f\x7f\x7f....", so it's around a ratio of 0.5
SessionRecord recordNoPqRatchet =
new SessionRecord(
Hex.fromStringCondensedAssert(
"0a29080332006a207f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7a0101"));
assertTrue(recordNoPqRatchet.hasSenderChain(0.0, Instant.EPOCH));
assertTrue(recordNoPqRatchet.hasSenderChain(0.25, Instant.EPOCH));
assertFalse(recordNoPqRatchet.hasSenderChain(0.75, Instant.EPOCH));
assertFalse(recordNoPqRatchet.hasSenderChain(1.0, Instant.EPOCH));
SessionRecord recordWithPq =
new SessionRecord(
Hex.fromStringCondensedAssert(
"0a29080432006a207f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7a0101"));
assertTrue(recordWithPq.hasSenderChain(0.0, Instant.EPOCH));
assertTrue(recordWithPq.hasSenderChain(0.25, Instant.EPOCH));
assertTrue(recordWithPq.hasSenderChain(0.75, Instant.EPOCH));
assertTrue(recordWithPq.hasSenderChain(1.0, Instant.EPOCH));
}
}

View File

@ -898,5 +898,13 @@
{
"version": "v0.93.1",
"size": 7515792
},
{
"version": "v0.93.2",
"size": 7515184
},
{
"version": "v0.94.0",
"size": 7522920
}
]

View File

@ -217,6 +217,8 @@ internal object Native {
@JvmStatic
public external fun AuthenticatedChatConnection_send_message_java(asyncRuntime: ObjectHandle, chat: ObjectHandle, destination: ByteArray, timestamp: Long, deviceIds: IntArray, registrationIds: IntArray, contents: Array<Object>, onlineOnly: Boolean, isUrgent: Boolean): CompletableFuture<Void?>
@JvmStatic
public external fun AuthenticatedChatConnection_send_raw_grpc(asyncRuntime: ObjectHandle, chat: ObjectHandle, service: String, method: String, payload: ByteArray): CompletableFuture<ByteArray>
@JvmStatic
public external fun AuthenticatedChatConnection_send_sync_message_java(asyncRuntime: ObjectHandle, chat: ObjectHandle, timestamp: Long, deviceIds: IntArray, registrationIds: IntArray, contents: Array<Object>, isUrgent: Boolean): CompletableFuture<Void?>
@JvmStatic @Throws(Exception::class)
@ -627,6 +629,8 @@ internal object Native {
@JvmStatic
public external fun KeyTransparency_E164SearchKey(e164: String): ByteArray
@JvmStatic
public external fun KeyTransparency_ResetDataField(accountData: ByteArray, field: Int): ByteArray
@JvmStatic
public external fun KeyTransparency_UsernameHashSearchKey(hash: ByteArray): ByteArray
@JvmStatic
@ -1207,7 +1211,7 @@ internal object Native {
@JvmStatic @Throws(Exception::class)
public external fun SessionRecord_GetSessionVersion(s: ObjectHandle): Int
@JvmStatic @Throws(Exception::class)
public external fun SessionRecord_HasUsableSenderChain(s: ObjectHandle, now: Long): Boolean
public external fun SessionRecord_HasUsableSenderChain(s: ObjectHandle, requirePqRatio: Double, now: Long): Boolean
@JvmStatic
public external fun SessionRecord_NewFresh(): ObjectHandle
@JvmStatic @Throws(Exception::class)
@ -1245,8 +1249,6 @@ internal object Native {
public external fun SignalMessage_GetSerialized(obj: ObjectHandle): ByteArray
@JvmStatic @Throws(Exception::class)
public external fun SignalMessage_New(messageVersion: Int, macKey: ByteArray, senderRatchetKey: ObjectHandle, counter: Int, previousCounter: Int, ciphertext: ByteArray, senderIdentityKey: ObjectHandle, receiverIdentityKey: ObjectHandle, pqRatchet: ByteArray): ObjectHandle
@JvmStatic @Throws(Exception::class)
public external fun SignalMessage_VerifyMac(msg: ObjectHandle, senderIdentityKey: ObjectHandle, receiverIdentityKey: ObjectHandle, macKey: ByteArray): Boolean
@JvmStatic @Throws(Exception::class)
public external fun SignedPreKeyRecord_Deserialize(data: ByteArray): ObjectHandle
@ -1309,6 +1311,8 @@ internal object Native {
public external fun UnauthenticatedChatConnection_send_message(asyncRuntime: ObjectHandle, chat: ObjectHandle, destination: ByteArray, timestamp: Long, deviceIds: IntArray, registrationIds: IntArray, contents: Array<ByteArray>, authKind: Int, authBuffer: ByteArray?, onlineOnly: Boolean, isUrgent: Boolean): CompletableFuture<Void?>
@JvmStatic
public external fun UnauthenticatedChatConnection_send_multi_recipient_message(asyncRuntime: ObjectHandle, chat: ObjectHandle, payload: ByteArray, timestamp: Long, auth: ByteArray?, onlineOnly: Boolean, isUrgent: Boolean): CompletableFuture<Array<Object>>
@JvmStatic
public external fun UnauthenticatedChatConnection_send_raw_grpc(asyncRuntime: ObjectHandle, chat: ObjectHandle, service: String, method: String, payload: ByteArray): CompletableFuture<ByteArray>
@JvmStatic @Throws(Exception::class)
public external fun UnidentifiedSenderMessageContent_Deserialize(data: ByteArray): ObjectHandle

View File

@ -116,7 +116,7 @@ public object NativeTesting {
@JvmStatic
public external fun TESTING_ErrorOnReturnSync(needsCleanup: Object): Object
@JvmStatic
public external fun TESTING_FakeChatConnection_Create(tokio: ObjectHandle, listener: BridgeChatListener, alertsJoinedByNewlines: String): ObjectHandle
public external fun TESTING_FakeChatConnection_Create(tokio: ObjectHandle, listener: BridgeChatListener, grpcOverridesJoinedByNewlines: String, alertsJoinedByNewlines: String): ObjectHandle
@JvmStatic
public external fun TESTING_FakeChatConnection_CreateProvisioning(tokio: ObjectHandle, listener: BridgeProvisioningListener): ObjectHandle
@JvmStatic
@ -128,14 +128,26 @@ public object NativeTesting {
@JvmStatic
public external fun TESTING_FakeChatConnection_TakeUnauthenticatedChat(chat: ObjectHandle): ObjectHandle
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_BinprotoToJson(name: String, input: ByteArray): String
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_GrpcFrameForMessageLength(len: Int): ByteArray
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_InjectConnectionInterrupted(chat: ObjectHandle): Unit
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_JsonToBinproto(name: String, input: String): ByteArray
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_NextGrpcMessage(input: ByteArray, offset: Int): Pair<Int, Int>
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_ReceiveIncomingGrpcRequest(asyncRuntime: ObjectHandle, chat: ObjectHandle): CompletableFuture<Pair<ObjectHandle, Long>?>
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_ReceiveIncomingRequest(asyncRuntime: ObjectHandle, chat: ObjectHandle): CompletableFuture<Pair<ObjectHandle, Long>?>
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_SendRawServerRequest(chat: ObjectHandle, bytes: ByteArray): Unit
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_SendRawServerResponse(chat: ObjectHandle, bytes: ByteArray): Unit
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_SendServerGrpcResponse(asyncRuntime: ObjectHandle, chat: ObjectHandle, response: ObjectHandle): CompletableFuture<Void?>
@JvmStatic
public external fun TESTING_FakeChatRemoteEnd_SendServerResponse(chat: ObjectHandle, response: ObjectHandle): Unit
@JvmStatic
public external fun TESTING_FakeChatResponse_Create(id: Long, status: Int, message: String, headers: Array<Object>, body: ByteArray?): ObjectHandle
@ -174,6 +186,8 @@ public object NativeTesting {
@JvmStatic @Throws(Exception::class)
public external fun TESTING_KeyTransNonFatalVerificationFailure(): Unit
@JvmStatic
public external fun TESTING_KeyTransStoredAccountData(): ByteArray
@JvmStatic
public external fun TESTING_NonSuspendingBackgroundThreadRuntime_Destroy(handle: ObjectHandle): Unit
@JvmStatic
public external fun TESTING_NonSuspendingBackgroundThreadRuntime_New(): ObjectHandle

View File

@ -7,11 +7,9 @@ package org.signal.libsignal.protocol.message;
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import javax.crypto.spec.SecretKeySpec;
import org.signal.libsignal.internal.CalledFromNative;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.InvalidVersionException;
@ -66,28 +64,6 @@ public class SignalMessage extends NativeHandleGuard.SimpleOwner
return filterExceptions(() -> guardedMapChecked(Native::SignalMessage_GetPqRatchet));
}
public void verifyMac(
IdentityKey senderIdentityKey, IdentityKey receiverIdentityKey, SecretKeySpec macKey)
throws InvalidMessageException, InvalidKeyException {
try (NativeHandleGuard guard = new NativeHandleGuard(this);
NativeHandleGuard senderIdentityGuard =
new NativeHandleGuard(senderIdentityKey.getPublicKey());
NativeHandleGuard receiverIdentityGuard =
new NativeHandleGuard(receiverIdentityKey.getPublicKey()); ) {
if (!filterExceptions(
InvalidMessageException.class,
InvalidKeyException.class,
() ->
Native.SignalMessage_VerifyMac(
guard.nativeHandle(),
senderIdentityGuard.nativeHandle(),
receiverIdentityGuard.nativeHandle(),
macKey.getEncoded()))) {
throw new InvalidMessageException("Bad Mac!");
}
}
}
@Override
public byte[] serialize() {
return filterExceptions(() -> guardedMapChecked(Native::SignalMessage_GetSerialized));

View File

@ -98,8 +98,8 @@ public class SessionRecord extends NativeHandleGuard.SimpleOwner {
*
* <p>If there is no current session, returns {@code false}.
*/
public boolean hasSenderChain() {
return hasSenderChain(Instant.now());
public boolean hasSenderChain(double requirePqRatio) {
return hasSenderChain(requirePqRatio, Instant.now());
}
/**
@ -109,12 +109,13 @@ public class SessionRecord extends NativeHandleGuard.SimpleOwner {
*
* <p>You should only use this overload if you need to test session expiration.
*/
public boolean hasSenderChain(Instant now) {
public boolean hasSenderChain(double requirePqRatio, Instant now) {
return filterExceptions(
() ->
guardedMapChecked(
(nativeHandle) ->
Native.SessionRecord_HasUsableSenderChain(nativeHandle, now.toEpochMilli())));
Native.SessionRecord_HasUsableSenderChain(
nativeHandle, requirePqRatio, now.toEpochMilli())));
}
public boolean currentRatchetKeyMatches(ECPublicKey key) {

View File

@ -14,7 +14,7 @@ generate-ffi:
swift/build_ffi.sh --generate-ffi
generate-node:
rust/bridge/node/bin/gen_ts_decl.py
cargo run -p libsignal-node-native_ts
alias generate-java := generate-jni
alias generate-swift := generate-ffi

View File

@ -1,12 +1,12 @@
{
"name": "@signalapp/libsignal-client",
"version": "0.93.2",
"version": "0.94.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@signalapp/libsignal-client",
"version": "0.93.2",
"version": "0.94.1",
"hasInstallScript": true,
"license": "AGPL-3.0-only",
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@signalapp/libsignal-client",
"version": "0.93.2",
"version": "0.94.1",
"repository": "github:signalapp/libsignal",
"license": "AGPL-3.0-only",
"type": "module",
@ -24,8 +24,8 @@
"build": "python3 build_node_bridge.py",
"build-with-debug-level-logs": "python3 build_node_bridge.py --debug-level-logs",
"clean": "rimraf dist build prebuilds",
"format": "p() { prettier ${@:- --write} '**/*.{css,js,json,md,scss,ts,tsx}' ../rust/bridge/node/bin/Native.ts.in; }; p",
"format-check": "p() { prettier ${@:- --check} '**/*.{css,js,json,md,scss,ts,tsx}' ../rust/bridge/node/bin/Native.ts.in; }; p",
"format": "p() { prettier ${@:- --write} '**/*.{css,js,json,md,scss,ts,tsx}'; }; p",
"format-check": "p() { prettier ${@:- --check} '**/*.{css,js,json,md,scss,ts,tsx}'; }; p",
"install": "echo Use \\`npm run build\\` to build the native library if needed",
"lint": "eslint .",
"prepack": "cp ../acknowledgments/acknowledgments-desktop.md dist/acknowledgments.md",

File diff suppressed because it is too large Load Diff

View File

@ -411,19 +411,6 @@ export class SignalMessage {
serialize(): Uint8Array<ArrayBuffer> {
return Native.SignalMessage_GetSerialized(this);
}
verifyMac(
senderIdentityKey: PublicKey,
recevierIdentityKey: PublicKey,
macKey: Uint8Array<ArrayBuffer>
): boolean {
return Native.SignalMessage_VerifyMac(
this,
senderIdentityKey,
recevierIdentityKey,
macKey
);
}
}
export class PreKeySignalMessage {
@ -518,8 +505,12 @@ export class SessionRecord {
*
* If there is no current session, returns false.
*/
hasCurrentState(now: Date = new Date()): boolean {
return Native.SessionRecord_HasUsableSenderChain(this, now.getTime());
hasCurrentState(requirePqRatio: number, now: Date = new Date()): boolean {
return Native.SessionRecord_HasUsableSenderChain(
this,
requirePqRatio,
now.getTime()
);
}
currentRatchetKeyMatches(key: PublicKey): boolean {

View File

@ -203,12 +203,15 @@ export class UnauthenticatedChatConnection implements ChatConnection {
*
* @param asyncContext the async runtime to use
* @param listener the listener to send events to
* @param grpcOverrides gRPC method names to prefer for typed APIs that have both WS and gRPC
* implementations.
* @returns an {@link UnauthenticatedChatConnection} and handle for the remote
* end of the fake connection.
*/
public static fakeConnect(
asyncContext: TokioAsyncContext,
listener: ChatServiceListener
listener: ChatServiceListener,
grpcOverrides?: ReadonlyArray<string>
): [UnauthenticatedChatConnection, FakeChatRemote] {
const nativeChatListener = makeNativeChatListener(asyncContext, listener);
@ -216,6 +219,7 @@ export class UnauthenticatedChatConnection implements ChatConnection {
Native.TESTING_FakeChatConnection_Create(
asyncContext,
new WeakListenerWrapper(nativeChatListener),
grpcOverrides?.join('\n') ?? '',
''
)
);
@ -319,13 +323,16 @@ export class AuthenticatedChatConnection implements ChatConnection {
*
* @param asyncContext the async runtime to use
* @param listener the listener to send events to
* @param grpcOverrides gRPC method names to prefer for typed APIs that have both WS and gRPC
* implementations.
* @param alerts alerts to send immediately upon connect
* @returns an {@link AuthenticatedChatConnection} and handle for the remote
* end of the fake connection.
* @returns an {@link AuthenticatedChatConnection} and handle for the remote end of the fake
* connection.
*/
public static fakeConnect(
asyncContext: TokioAsyncContext,
listener: ChatServiceListener,
grpcOverrides?: ReadonlyArray<string>,
alerts?: ReadonlyArray<string>
): [AuthenticatedChatConnection, FakeChatRemote] {
const nativeChatListener = makeNativeChatListener(asyncContext, listener);
@ -334,6 +341,7 @@ export class AuthenticatedChatConnection implements ChatConnection {
Native.TESTING_FakeChatConnection_Create(
asyncContext,
new WeakListenerWrapper(nativeChatListener),
grpcOverrides?.join('\n') ?? '',
alerts?.join('\n') ?? ''
)
);

View File

@ -168,6 +168,46 @@ export interface Client {
) => Promise<void>;
}
/**
* A tag identifying an optional field of the account data.
*
* (Must be in sync with the Rust counterpart)
*/
export enum AccountDataField {
E164 = 0,
UsernameHash = 1,
}
/**
* Resets a particular field in the data associated with given ACI.
*
* Must only be called for the "self" account when either E.164 or username
* change is performed.
*
* Upon successful completion the data associated with the account will be
* updated in the store, if it was present to begin with, noop if it was not.
*
* @param aci - An ACI of "self" account.
* @param field - Account data field to be reset (E.164 or username hash).
* @param store - local persistent storage for key transparency-related data.
* @throws {TypeError} if the stored data cannot be decoded correctly, which means data corruption.
*/
export async function resetField(
aci: Aci,
field: AccountDataField,
store: Store
): Promise<void> {
const accountData = await store.getAccountData(aci);
if (accountData === null) {
return;
}
const updated = Native.KeyTransparency_ResetDataField(accountData, field);
if (updated.length === 0) {
throw new TypeError('failed to decode account data');
}
await store.setAccountData(aci, updated);
}
export class ClientImpl implements Client {
constructor(
private readonly asyncContext: TokioAsyncContext,

View File

@ -90,6 +90,33 @@ describe('KeyTransparency bridging', () => {
});
});
describe('KeyTransparency.resetField', () => {
it('throws on corrupt data', async () => {
const store = new InMemoryKtStore();
await store.setAccountData(testAci, new Uint8Array([1, 2, 3]));
await expect(
KT.resetField(testAci, KT.AccountDataField.E164, store)
).to.be.rejectedWith(TypeError);
});
it('is a noop when data is missing', async () => {
const store = new InMemoryKtStore();
await KT.resetField(testAci, KT.AccountDataField.E164, store);
expect(store.storage.get(testAci)).to.equal(undefined);
});
it('updates store on success', async () => {
const store = new InMemoryKtStore();
await store.setAccountData(
testAci,
Native.TESTING_KeyTransStoredAccountData()
);
expect(store.storage.get(testAci)).to.have.lengthOf(1);
await KT.resetField(testAci, KT.AccountDataField.E164, store);
expect(store.storage.get(testAci)).to.have.lengthOf(2);
});
});
describe('KeyTransparency network errors', () => {
it('can bridge network errors', async () => {
async function run(statusCode: number, headers: string[] = []) {

View File

@ -529,6 +529,7 @@ describe('chat service api', () => {
const [_chat, fakeRemote] = AuthenticatedChatConnection.fakeConnect(
tokio,
listener,
[],
['UPPERcase', 'lowercase']
);

View File

@ -812,7 +812,7 @@ for (const testCase of sessionVersionTestCases) {
assert(session.serialize().length > 0);
assert.deepEqual(session.localRegistrationId(), 5);
assert.deepEqual(session.remoteRegistrationId(), 5);
assert(session.hasCurrentState());
assert(session.hasCurrentState(1.0));
assert(
!session.currentRatchetKeyMatches(
SignalClient.PrivateKey.generate().getPublicKey()
@ -820,7 +820,7 @@ for (const testCase of sessionVersionTestCases) {
);
session.archiveCurrentState();
assert(!session.hasCurrentState());
assert(!session.hasCurrentState(1.0));
assert(
!session.currentRatchetKeyMatches(
SignalClient.PrivateKey.generate().getPublicKey()
@ -968,8 +968,12 @@ for (const testCase of sessionVersionTestCases) {
);
const initialSession = await aliceStores.session.getSession(bAddress);
assert.isTrue(initialSession?.hasCurrentState(new Date('2020-01-01')));
assert.isFalse(initialSession?.hasCurrentState(new Date('2023-01-01')));
assert.isTrue(
initialSession?.hasCurrentState(1.0, new Date('2020-01-01'))
);
assert.isFalse(
initialSession?.hasCurrentState(1.0, new Date('2023-01-01'))
);
const aMessage = Buffer.from('Greetings hoo-man', 'utf8');
const aCiphertext = await SignalClient.signalEncrypt(
@ -987,8 +991,12 @@ for (const testCase of sessionVersionTestCases) {
);
const updatedSession = await aliceStores.session.getSession(bAddress);
assert.isTrue(updatedSession?.hasCurrentState(new Date('2020-01-01')));
assert.isFalse(updatedSession?.hasCurrentState(new Date('2023-01-01')));
assert.isTrue(
updatedSession?.hasCurrentState(1.0, new Date('2020-01-01'))
);
assert.isFalse(
updatedSession?.hasCurrentState(1.0, new Date('2023-01-01'))
);
await assert.isRejected(
SignalClient.signalEncrypt(

View File

@ -80,8 +80,8 @@ class definition. For Swift, the `cbindgen` output is saved directly to a
C-style `.h` file that the Swift toolchain can consume.
For TypeScript, the [`libsignal-node`] crate is expanded and processed by
[`gen_ts_decl.py`](./node/bin/gen_ts_decl.py) and the output is interpolated into
[`Native.ts.in`](./node/bin/Native.ts.in). The output, however, only
[`libsignal-node-native_ts`](./node/native_ts/src/main.rs) and the output is interpolated into
[`Native.ts.in`](./node/native_ts/src/Native.ts.in). The output, however, only
declares the function signatures; to make them accessible to the JavaScript
runtime, additional machinery is used. This takes the form of `#[linkme]`
annotations on to the generated entry points; the [`linkme`] crate is used to

View File

@ -43,6 +43,7 @@ exclude = [
"CPromisebool",
"CPromiseFfiCdsiLookupResponse",
"CPromiseMutPointerRegistrationService",
"CPromiseOwnedBufferOfc_uchar",
"FfiCdsiLookupResponse",
"FfiCdsiLookupResponseEntry",
"FfiChatListenerStruct",

View File

@ -82,6 +82,7 @@ def translate_to_java(typ: str) -> Tuple[str, bool]:
'Nullable<ObjectHandle>': 'ObjectHandle',
'jint': 'Int',
'jlong': 'Long',
'jdouble': 'Double',
'jboolean': 'Boolean',
'JObject': 'Object',
'JClass': 'Class<*>',

View File

@ -15,17 +15,23 @@ workspace = true
[lib]
name = "signal_node"
crate-type = ["cdylib"]
crate-type = ["cdylib", "lib"]
[features]
# Here for bridge_fn uniformity
node = []
default = ["node"]
metadata = [
"libsignal-bridge/metadata",
"libsignal-bridge-testing/metadata",
"libsignal-bridge-types/metadata",
]
[dependencies]
libsignal-bridge = { workspace = true, features = ["node", "signal-media"] }
libsignal-bridge-macros = { workspace = true }
libsignal-bridge-testing = { workspace = true, features = ["node", "signal-media"] }
libsignal-bridge-types = { workspace = true, features = ["node"] }
libsignal-protocol = { workspace = true }
futures = { workspace = true }

View File

@ -1,357 +0,0 @@
#!/usr/bin/env python3
#
# Copyright (C) 2020-2021 Signal Messenger, LLC.
# SPDX-License-Identifier: AGPL-3.0-only
#
import collections
import difflib
import itertools
import os
import re
import subprocess
import sys
from typing import Iterable, Iterator, Tuple
Args = collections.namedtuple('Args', ['verify'])
def parse_args() -> Args:
def print_usage_and_exit() -> None:
print('usage: %s [--verify]' % sys.argv[0], file=sys.stderr)
sys.exit(2)
# If the command-line handling below gets any more complicated, this should be switched to argparse.
mode = None
if len(sys.argv) > 2:
print_usage_and_exit()
elif len(sys.argv) == 2:
mode = sys.argv[1]
if mode != '--verify':
print_usage_and_exit()
return Args(verify=mode is not None)
def split_rust_args(args: str) -> Iterator[Tuple[str, str]]:
"""
Split Rust `arg: Type` pairs separated by commas.
Account for templates, tuples, and slices.
"""
while ': ' in args:
(name, args) = args.split(': ', maxsplit=1)
if name.startswith('mut '):
name = name[4:]
open_pairs = 0
for (i, c) in enumerate(args):
if c == ',' and open_pairs == 0:
ty = args[:i]
args = args[i + 1:]
yield (name.strip(), ty.strip())
break
elif c in ['<', '(', '[']:
open_pairs += 1
elif c in ['>', ')', ']']:
open_pairs -= 1
else:
yield (name.strip(), args.strip())
def translate_to_ts(typ: str) -> str:
typ = typ.replace(' ', '')
type_map = {
'()': 'void',
'&[u8]': 'Uint8Array<ArrayBuffer>',
'i32': 'number',
'u8': 'number',
'u16': 'number',
'u32': 'number',
'u64': 'bigint',
'bool': 'boolean',
'String': 'string',
'&str': 'string',
'Vec<u8>': 'Uint8Array<ArrayBuffer>',
'Box<[u8]>': 'Uint8Array<ArrayBuffer>',
'Box<[u32]>': 'Uint32Array<ArrayBuffer>',
'bytes::Bytes': 'Uint8Array<ArrayBuffer>',
'ServiceId': 'Uint8Array<ArrayBuffer>',
'Aci': 'Uint8Array<ArrayBuffer>',
'Pni': 'Uint8Array<ArrayBuffer>',
'E164': 'string',
"ServiceIdSequence<'_>": 'Uint8Array<ArrayBuffer>',
'PathAndQuery': 'string',
'LanguageList': 'string[]',
'GroupSendFullToken': 'Uint8Array<ArrayBuffer>',
'DeviceSpecifier': 'number',
'&BackupKey': 'Uint8Array<ArrayBuffer>',
'MultiRecipientSendAuthorization': 'Uint8Array<ArrayBuffer> | null',
'DisconnectCause': 'Error | null',
'::zkgroup::backups::BackupAuthCredential': 'Uint8Array<ArrayBuffer>',
'::zkgroup::generic_server_params::GenericServerPublicParams': 'Uint8Array<ArrayBuffer>',
}
if typ in type_map:
return type_map[typ]
if typ.startswith('[u8;') or typ.startswith('&[u8;'):
return 'Uint8Array<ArrayBuffer>'
if typ.startswith('&mutdyn'):
return typ[7:]
if typ.startswith('&dyn'):
return typ[4:]
if typ.startswith('&mut'):
return 'Wrapper<' + typ[4:] + '>'
if typ.startswith('&[&'):
assert typ.endswith(']')
return 'Wrapper<' + translate_to_ts(typ[3:-1]) + '>[]'
if typ.startswith('Box<['):
assert typ.endswith(']>')
return translate_to_ts(typ[5:-2]) + '[]'
if typ.startswith('Box<dyn'):
assert typ.endswith('>')
return translate_to_ts(typ[7:-1])
if typ.startswith('Vec<'):
assert typ.endswith('>')
return translate_to_ts(typ[4:-1]) + '[]'
if typ.startswith('&['):
assert typ.endswith(']')
return 'Wrapper<' + translate_to_ts(typ[2:-1]) + '>[]'
if typ.startswith('&'):
return 'Wrapper<' + typ[1:] + '>'
if typ.startswith('('):
assert typ.endswith(')'), typ
inner = typ[1:-1].split(',')
if len(inner) == 1:
return translate_to_ts(inner[0])
return '[' + ', '.join(translate_to_ts(x) for x in inner) + ']'
if typ.startswith('Option<'):
assert typ.endswith('>')
return translate_to_ts(typ[7:-1]) + ' | null'
if typ.startswith('Result<'):
assert typ.endswith('>')
type_args = typ[7:-1]
(success_type, *failure_type) = type_args.rsplit(',', 1)
if failure_type and ')' in failure_type[0]:
success_type = type_args
return translate_to_ts(success_type)
if typ.startswith('std::result::Result<'):
assert typ.endswith('>')
type_args = typ[20:-1]
(success_type, *failure_type) = type_args.rsplit(',', 1)
if failure_type and ')' in failure_type[0]:
success_type = type_args
return translate_to_ts(success_type)
if typ.startswith('Promise<'):
assert typ.endswith('>')
return 'Promise<' + translate_to_ts(typ[8:-1]) + '>'
if typ.startswith('CancellablePromise<'):
assert typ.endswith('>')
return 'CancellablePromise<' + translate_to_ts(typ[19:-1]) + '>'
if typ.startswith('AsType<'):
assert typ.endswith('>')
assert ',' in typ
return translate_to_ts(typ.split(',')[1][:-1])
if typ.startswith('Ignored<'):
assert typ.endswith('>')
return 'null'
return typ
DIAGNOSTICS_TO_IGNORE = [
r'warning: \d+ warnings? emitted',
r'warning: unused import',
r'warning: field.+ never read',
r'warning: variant.+ never constructed',
r'warning: method.+ never used',
r'warning: associated function.+ never used',
]
SHOULD_IGNORE_PATTERN = re.compile('(' + ')|('.join(DIAGNOSTICS_TO_IGNORE) + ')')
def camelcase(arg: str) -> str:
return re.sub(
# Preserve double-underscores and leading underscores,
# but remove single underscores and capitalize the following letter.
r'([^_])_([^_])',
lambda match: match.group(1) + match.group(2).upper(),
arg)
def rewrite_function_as_property(ts_function: str) -> str:
return ts_function.replace('(', ': (', 1).replace('):', ') =>')
def rewrite_fn(function_match: re.Match[str]) -> str:
(prefix, fn_args, ret_type) = function_match.groups()
ts_ret_type = translate_to_ts(ret_type)
ts_args = []
for (arg_name, arg_type) in split_rust_args(fn_args):
ts_arg_type = translate_to_ts(arg_type)
ts_args.append('%s: %s' % (camelcase(arg_name.strip()), ts_arg_type))
return '%s(%s): %s;' % (prefix, ', '.join(ts_args), ts_ret_type)
def rewrite_trait(decl: str, function_sig: re.Pattern[str]) -> Iterator[str]:
for line in decl.split('\\n'):
if function_match := function_sig.match(line.rstrip(';')):
yield ' ' + rewrite_function_as_property(rewrite_fn(function_match))
continue
# Fix backslash-escaped double-quotes.
yield bytes(line, 'utf-8').decode('unicode_escape')
def collect_decls(crate_dir: str, features: Iterable[str] = ()) -> Iterator[str]:
args = [
'cargo',
'rustc',
'-q',
'--profile=check',
'--features', ','.join(features),
'--message-format=short',
'--color=never',
'--',
'-Zunpretty=expanded']
rustc = subprocess.Popen(args, cwd=crate_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(raw_stdout, raw_stderr) = rustc.communicate()
stdout = str(raw_stdout.decode('utf8'))
stderr = str(raw_stderr.decode('utf8'))
had_error = False
for l in stderr.split('\n'):
if l == '':
continue
if SHOULD_IGNORE_PATTERN.search(l):
continue
print(l, file=sys.stderr)
had_error = True
if had_error:
print('Exiting with error')
sys.exit(1)
comment_decl = re.compile(r'\s*///\s*ts: `(.+)`')
# Note that the doc attribute is sometimes wrapped onto two lines.
attr_decl = re.compile(r'\s*(?:#\[doc\s*=\s*)?"ts: `(.+)`"\]')
# Make sure /not/ to match arguments with nested parentheses,
# which won't survive textual splitting below.
function_sig = re.compile(r'(.+)\(([^()]*)\): (.+)')
for line in stdout.split('\n'):
match = comment_decl.match(line) or attr_decl.match(line)
if match is None:
continue
(decl,) = match.groups()
if decl.startswith('export /*trait*/ type '):
yield '\n'.join(rewrite_trait(decl, function_sig))
continue
if function_match := function_sig.match(decl):
yield rewrite_fn(function_match)
continue
# Fix backslash-escaped double-quotes.
yield bytes(decl, 'utf-8').decode('unicode_escape')
def expand_template(template_file: str, decls: Iterable[str]) -> str:
decls = list(decls)
with open(template_file, 'r') as f:
contents = f.read()
# Rewrite from function syntax to property syntax to take advantage of
# https://www.typescriptlang.org/tsconfig/#strictFunctionTypes.
contents = contents.replace('NATIVE_FNS;', '\n '.join(
rewrite_function_as_property(x.removeprefix('export function '))
for x in decls if x.startswith('export function ')
))
contents = contents.replace('NATIVE_FN_NAMES', ''.join(
'\n ' + x.removeprefix('export function ').split('(')[0] + ','
for x in decls if x.startswith('export function ')
) + '\n')
contents = contents.replace('NATIVE_TYPES;', '\n'.join(
'export ' + x.removeprefix('export ') for x in decls if not x.startswith('export function ')
))
return contents
def verify_contents(expected_output_file: str, expected_contents: str) -> None:
with open(expected_output_file) as fh:
current_contents = fh.readlines()
diff = difflib.unified_diff(current_contents, expected_contents.splitlines(keepends=True))
first_line = next(diff, None)
if first_line:
sys.stdout.write(first_line)
sys.stdout.writelines(diff)
sys.exit(f'error: {expected_output_file} not up to date; re-run {sys.argv[0]}!')
Crate = collections.namedtuple('Crate', ['path', 'features'], defaults=[()])
def convert_to_typescript(rust_crates: Iterable[Crate], ts_in_path: str, ts_out_path: str, verify: bool) -> None:
decls = itertools.chain.from_iterable(collect_decls(crate.path, crate.features) for crate in rust_crates)
contents = expand_template(ts_in_path, decls)
if not os.access(ts_out_path, os.F_OK):
raise Exception(f"Didn't find {ts_out_path} where it was expected")
if not verify:
with open(ts_out_path, 'w') as fh:
fh.write(contents)
else:
verify_contents(ts_out_path, contents)
def main() -> None:
args = parse_args()
our_abs_dir = os.path.dirname(os.path.realpath(__file__))
output_file_name = 'Native.ts'
convert_to_typescript(
rust_crates=[
Crate(path=os.path.join(our_abs_dir, '..')),
Crate(path=os.path.join(our_abs_dir, '..', '..', 'shared'), features=('node', 'signal-media')),
Crate(path=os.path.join(our_abs_dir, '..', '..', 'shared', 'types'), features=('node', 'signal-media')),
Crate(path=os.path.join(our_abs_dir, '..', '..', 'shared', 'testing'), features=('node', 'signal-media')),
],
ts_in_path=os.path.join(our_abs_dir, output_file_name + '.in'),
ts_out_path=os.path.join(our_abs_dir, '..', '..', '..', '..', 'node', 'ts', output_file_name),
verify=args.verify,
)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,19 @@
[package]
name = "libsignal-node-native_ts"
version = "0.1.0"
authors.workspace = true
license.workspace = true
edition = "2024"
[lints]
workspace = true
[dependencies]
libsignal-bridge = { workspace = true, features = ["node", "metadata"] }
libsignal-bridge-testing = { workspace = true, features = ["node", "metadata"] }
libsignal-bridge-types = { workspace = true, features = ["node", "metadata"] }
libsignal-node = { workspace = true, features = ["node", "metadata"] }
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
minijinja = { workspace = true, features = ["preserve_order"] }

View File

@ -1,5 +1,5 @@
//
// Copyright 2020 Signal Messenger, LLC.
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
@ -134,18 +134,60 @@ export type Serialized<T> = Uint8Array<ArrayBuffer>;
type ConnectChatBridge = Wrapper<ConnectionManager>;
type TestingFutureCancellationGuard = Wrapper<TestingFutureCancellationCounter>;
// Keep in sync with rust/bridge/node/src/logging.rs
export const enum LogLevel { Error = 1, Warn, Info, Debug, Trace }
/* eslint-disable comma-dangle */
export const NetRemoteConfigKeys = [
{%- for key in remote_config_keys -%}
'{{ key }}',
{%- endfor -%}
] as const;
import load from 'node-gyp-build';
type NativeFunctions = {
registerErrors: (errorsModule: Record<string, unknown>) => void;
NATIVE_FNS;
initLogger: (maxLevel: LogLevel, callback: (level: LogLevel, target: string, file: string | null, line: number | null, message: string) => void) => void;
{%- for (name, f) in ctx.native_functions|items %}
{{ name }}: (
{%- for (name, ty) in f.arguments -%}
{{ name }}: {{ ty }},
{%- endfor -%}
) => {{ f.return_type }};
{%- endfor %}
};
const { registerErrors, NATIVE_FN_NAMES } = load(
{% macro native_fn_names(ctx) %}
{%- for (name, f) in ctx.native_functions|items %}
{{ name }},
{%- endfor %}
{% endmacro %}
const { registerErrors,
initLogger,
{{ native_fn_names(ctx) }}
} = load(
`${import.meta.dirname}/../`
) as NativeFunctions;
export { registerErrors, NATIVE_FN_NAMES };
export { registerErrors,
initLogger,
{{ native_fn_names(ctx)
}} };
/* eslint-disable comma-dangle */
NATIVE_TYPES;
{% for (name, fns) in ctx.bridge_traits|items %}
export /*trait*/ type {{ name }} = {
{%- for fn in fns %}
{{ fn.name }}: (
{%- for (arg, ty) in fn.body.arguments -%}
{{ arg }}: {{ ty }},
{%- endfor -%}
) => {{ fn.body.return_type }};
{%- endfor %}
};
{% endfor %}
{% for ty in ctx.opaque_types -%}
export interface {{ ty }} { readonly __type: unique symbol; }
{% endfor -%}

View File

@ -0,0 +1,51 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
// To make sure the linkmes work
extern crate libsignal_bridge;
extern crate libsignal_bridge_testing;
extern crate signal_node;
use clap::Parser;
use libsignal_bridge_types::metadata::node::TsMetadataContext;
use libsignal_bridge_types::net::remote_config::RemoteConfigKey;
use minijinja::context;
#[derive(Parser)]
/// Regenerate Native.ts
///
/// This command assumes it's being invoked from the workspace root.
struct Cli {
/// Don't actually overwrite Native.ts, just make sure it's up-to-date.
#[clap(long)]
verify: bool,
}
fn main() -> anyhow::Result<()> {
let args = Cli::parse();
let mut env = minijinja::Environment::new();
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
env.add_template("Native.ts.in", include_str!("Native.ts.in"))?;
let tmpl = env.get_template("Native.ts.in")?;
let mut ctx = TsMetadataContext::default();
for item in libsignal_bridge_types::metadata::node::NODE_ITEMS.iter() {
// We don't check item.module_path because, unlike other client languages, we emit both
// testing and non-testing native into the same typescript file.
(item.apply)(&mut ctx);
}
let code = tmpl.render(context! {
ctx => ctx,
remote_config_keys => RemoteConfigKey::KEYS,
})?;
let dst = "./node/ts/Native.ts";
if args.verify {
anyhow::ensure!(
std::fs::read_to_string(dst)? == code,
"Native.ts is not up-to-date"
);
} else {
std::fs::write(dst, code.as_bytes())?;
}
Ok(())
}

View File

@ -6,7 +6,7 @@
#![warn(clippy::unwrap_used)]
use futures::executor;
use libsignal_bridge::node::{AssumedImmutableBuffer, ResultTypeInfo, SignalNodeError};
use libsignal_bridge::node::ResultTypeInfo;
use libsignal_bridge::node_register;
use libsignal_bridge::support::*;
use libsignal_bridge_macros::bridge_fn;
@ -16,7 +16,6 @@ use minidump_processor::ProcessorOptions;
use minidump_unwind::Symbolizer;
use minidump_unwind::symbols::string_symbol_supplier;
use neon::prelude::*;
use neon::types::buffer::TypedArray;
use rand::TryRngCore;
use uuid::Uuid;
@ -31,11 +30,6 @@ use libsignal_bridge_testing::*;
fn main(mut cx: ModuleContext) -> NeonResult<()> {
libsignal_bridge::node::register(&mut cx)?;
cx.export_function("initLogger", logging::init_logger)?;
cx.export_function(
"SealedSenderMultiRecipientMessage_Parse",
sealed_sender_multi_recipient_message_parse,
)?;
cx.export_function("MinidumpToJSONString", minidump_to_json_string)?;
let remote_config_keys = libsignal_bridge::net::RemoteConfigKey::KEYS.convert_into(&mut cx)?;
cx.export_value("NetRemoteConfigKeys", remote_config_keys)?;
Ok(())
@ -67,91 +61,96 @@ impl<'a> From<ArrayBuilder<'a>> for Handle<'a, JsArray> {
}
}
/// ts: `export function SealedSenderMultiRecipientMessage_Parse(buffer: Uint8Array<ArrayBuffer>): SealedSenderMultiRecipientMessage`
fn sealed_sender_multi_recipient_message_parse(mut cx: FunctionContext) -> JsResult<JsObject> {
let buffer_arg = cx.argument::<JsUint8Array>(0)?;
let buffer = AssumedImmutableBuffer::new(&cx, buffer_arg);
let messages = match SealedSenderV2SentMessage::parse(&buffer) {
Ok(messages) => messages,
Err(e) => {
let throwable =
e.into_throwable(&mut cx, "sealed_sender_multi_recipient_parse_sent_message");
cx.throw(throwable)?
}
};
struct SealedSenderMultiRecipientMessage<'a>(SealedSenderV2SentMessage<'a>);
impl<'a, 'b> ResultTypeInfo<'a> for SealedSenderMultiRecipientMessage<'b> {
type ResultType = JsObject;
let recipient_map = cx.empty_object();
let mut excluded_recipients_array = ArrayBuilder::new(&mut cx);
fn convert_into(self, cx: &mut impl Context<'a>) -> JsResult<'a, Self::ResultType> {
let messages = self.0;
let recipient_map = cx.empty_object();
let mut excluded_recipients_array = ArrayBuilder::new(cx);
for (service_id, recipient) in &messages.recipients {
let service_id_string = cx.string(service_id.service_id_string());
if recipient.devices.is_empty() {
excluded_recipients_array
.push(service_id_string, &mut cx)
.expect("failed to construct output array");
continue;
for (service_id, recipient) in &messages.recipients {
let service_id_string = cx.string(service_id.service_id_string());
if recipient.devices.is_empty() {
excluded_recipients_array
.push(service_id_string, cx)
.expect("failed to construct output array");
continue;
}
let mut device_ids = ArrayBuilder::new(cx);
let mut registration_ids = ArrayBuilder::new(cx);
for &(device_id, registration_id) in &recipient.devices {
device_ids
.push(cx.number(u32::from(device_id)), cx)
.expect("failed to construct output array");
registration_ids
.push(cx.number(registration_id), cx)
.expect("failed to construct output array");
}
let range = messages.range_for_recipient_key_material(recipient);
let range_start = cx.number(u32::try_from(range.start).expect("message too large"));
let range_len = cx.number(u32::try_from(range.len()).expect("message too large"));
let recipient_object = cx.empty_object();
recipient_object
.set(cx, "deviceIds", device_ids.into())
.expect("failed to construct recipient object");
recipient_object
.set(cx, "registrationIds", registration_ids.into())
.expect("failed to construct recipient object");
recipient_object
.set(cx, "rangeOffset", range_start)
.expect("failed to construct recipient object");
recipient_object
.set(cx, "rangeLen", range_len)
.expect("failed to construct recipient object");
recipient_map
.set(cx, service_id_string, recipient_object)
.expect("failed to record recipient object");
}
let mut device_ids = ArrayBuilder::new(&mut cx);
let mut registration_ids = ArrayBuilder::new(&mut cx);
let offset_of_shared_bytes =
cx.number(u32::try_from(messages.offset_of_shared_bytes()).expect("message too large"));
for &(device_id, registration_id) in &recipient.devices {
device_ids
.push(cx.number(u32::from(device_id)), &mut cx)
.expect("failed to construct output array");
registration_ids
.push(cx.number(registration_id), &mut cx)
.expect("failed to construct output array");
}
let result = cx.empty_object();
result
.set(cx, "recipientMap", recipient_map)
.expect("failed to construct result object");
result
.set(cx, "excludedRecipients", excluded_recipients_array.into())
.expect("failed to construct result object");
result
.set(cx, "offsetOfSharedData", offset_of_shared_bytes)
.expect("failed to construct result object");
let range = messages.range_for_recipient_key_material(recipient);
let range_start = cx.number(u32::try_from(range.start).expect("message too large"));
let range_len = cx.number(u32::try_from(range.len()).expect("message too large"));
let recipient_object = cx.empty_object();
recipient_object
.set(&mut cx, "deviceIds", device_ids.into())
.expect("failed to construct recipient object");
recipient_object
.set(&mut cx, "registrationIds", registration_ids.into())
.expect("failed to construct recipient object");
recipient_object
.set(&mut cx, "rangeOffset", range_start)
.expect("failed to construct recipient object");
recipient_object
.set(&mut cx, "rangeLen", range_len)
.expect("failed to construct recipient object");
recipient_map
.set(&mut cx, service_id_string, recipient_object)
.expect("failed to record recipient object");
Ok(result)
}
let offset_of_shared_bytes =
cx.number(u32::try_from(messages.offset_of_shared_bytes()).expect("message too large"));
let result = cx.empty_object();
result
.set(&mut cx, "recipientMap", recipient_map)
.expect("failed to construct result object");
result
.set(
&mut cx,
"excludedRecipients",
excluded_recipients_array.into(),
)
.expect("failed to construct result object");
result
.set(&mut cx, "offsetOfSharedData", offset_of_shared_bytes)
.expect("failed to construct result object");
Ok(result)
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(
_: &mut libsignal_bridge_types::metadata::node::TsMetadataContext,
) -> String {
"SealedSenderMultiRecipientMessage".into()
}
}
/// ts: `export function MinidumpToJSONString(buffer: Uint8Array<ArrayBuffer>): string`
fn minidump_to_json_string(mut cx: FunctionContext) -> JsResult<JsString> {
let buffer_arg = cx.argument::<JsUint8Array>(0)?;
let dump = Minidump::read(buffer_arg.as_slice(&cx)).expect("Failed to parse minidump");
#[bridge_fn(jni = false, ffi = false)]
fn SealedSenderMultiRecipientMessage_Parse(
buffer: &[u8],
) -> libsignal_protocol::error::Result<SealedSenderMultiRecipientMessage<'_>> {
Ok(SealedSenderMultiRecipientMessage(
SealedSenderV2SentMessage::parse(buffer)?,
))
}
#[bridge_fn(ffi = false, jni = false)]
fn MinidumpToJSONString(buffer: &[u8]) -> String {
let dump = Minidump::read(buffer).expect("Failed to parse minidump");
let provider = Symbolizer::new(string_symbol_supplier(std::collections::HashMap::new()));
let options = ProcessorOptions::default();
@ -165,7 +164,7 @@ fn minidump_to_json_string(mut cx: FunctionContext) -> JsResult<JsString> {
.print_json(&mut json, false)
.expect("Failed to print json");
Ok(cx.string(std::str::from_utf8(&json).expect("Failed to convert JSON to utf8")))
String::from_utf8(json).expect("Failed to convert JSON to utf8")
}
#[bridge_fn(ffi = false, jni = false)]

View File

@ -9,7 +9,7 @@ use std::sync::atomic::AtomicBool;
use libsignal_bridge::node::SimpleArgTypeInfo;
use neon::prelude::*;
/// ts: `export const enum LogLevel { Error = 1, Warn, Info, Debug, Trace }`
// Keep in sync with Native.ts.in
#[derive(Clone, Copy)]
enum LogLevel {
Error = 1,
@ -194,7 +194,6 @@ fn set_max_level_from_js_level(max_level: u32) {
log::set_max_level(log::Level::from(level).to_level_filter());
}
/// ts: `export function initLogger(maxLevel: LogLevel, callback: (level: LogLevel, target: string, file: string | null, line: number | null, message: string) => void): void`
pub(crate) fn init_logger(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let max_level_arg = cx.argument::<JsNumber>(0)?;
let max_level = u32::convert_from(&mut cx, max_level_arg)?;

View File

@ -71,3 +71,4 @@ ffi = ["libsignal-bridge-types/ffi"]
jni = ["dep:jni", "libsignal-bridge-types/jni"]
node = ["neon", "linkme", "libsignal-bridge-types/node"]
signal-media = ["dep:signal-media", "libsignal-bridge-types/signal-media"]
metadata = ["libsignal-bridge-types/metadata"]

View File

@ -209,7 +209,7 @@ pub(crate) fn bridge_trait(trait_to_bridge: &ItemTrait, name: &str) -> Result<To
let callbacks = trait_to_bridge
.items
.iter()
.map(|item| bridge_callback_item(name, item))
.map(|item| bridge_callback_item(trait_name, name, item))
.collect::<Result<Vec<_>>>()?;
let callback_aliases = callbacks.iter().map(|c| &c.alias);
let callback_fields = callbacks.iter().map(|c| &c.field);
@ -264,7 +264,11 @@ struct Callback {
forwarding_impl: TokenStream2,
}
fn bridge_callback_item(bridge_name: &str, item: &TraitItem) -> Result<Callback> {
fn bridge_callback_item(
trait_name: &Ident,
bridge_name: &str,
item: &TraitItem,
) -> Result<Callback> {
let TraitItem::Fn(item) = item else {
return Err(Error::new(item.span(), "only fns are supported"));
};
@ -281,6 +285,12 @@ fn bridge_callback_item(bridge_name: &str, item: &TraitItem) -> Result<Callback>
req_name.to_string().to_upper_camel_case(),
span = req_name.span()
);
let mut_keyword = item.sig.inputs.first().and_then(|input| match input {
FnArg::Receiver(receiver) => receiver.mutability.as_ref(),
FnArg::Typed(_) => None,
});
let callback_args = item.sig.inputs.iter().filter_map(|arg| match arg {
FnArg::Receiver(_) => match result_info.kind {
ResultKind::Regular => Some(quote!(out: *mut ffi_arg_type!(#result_ty))),
@ -384,7 +394,7 @@ fn bridge_callback_item(bridge_name: &str, item: &TraitItem) -> Result<Callback>
let forwarding_impl = quote! {
#[inline]
#sig {
self.0.#req_name(#(#arg_names),*) #await_if_needed
#trait_name::#req_name(& #mut_keyword self.0, #(#arg_names),*) #await_if_needed
}
};

View File

@ -52,7 +52,7 @@
//! export function SenderKeyMessage_New(
//! keyId: number,
//! iteration: number,
//! ciphertext: Buffer,
//! ciphertext: Uint8Array<ArrayBuffer>,
//! pk: Wrapper<PrivateKey>
//! ): SenderKeyMessage;
//! ```
@ -132,8 +132,9 @@
//!
//! 1. Argument and result types for FFI and JNI are determined by macros `ffi_arg_type`,
//! `ffi_result_type`, `jni_arg_type`, and `jni_result_type`. You may need to add your new type
//! there. JNI and Node types also undergo some additional transformation in the scripts
//! `gen_java_decl.py` and `gen_ts_decl.py`, which you may need to tweak as well.
//! there. JNI types also undergo some additional transformation in the scripts
//! `gen_java_decl.py`, which you may need to tweak as well. Node types are generated as Strings
//! via the `gen_ts_ffi()` methods on `node::{AsyncArg, Arg, Result}TypeInfo`.
//!
//! 2. Argument types conform to one or more of the following bridge-specific traits:
//!

View File

@ -12,7 +12,7 @@ use syn::*;
use syn_mid::Signature;
use crate::BridgingKind;
use crate::util::{extract_arg_names_and_types, result_type};
use crate::util::{crates, extract_arg_names_and_types, result_type};
fn bridge_fn_body(orig_name: &Ident, input_args: &[(&Ident, &Type)]) -> TokenStream2 {
// Scroll down to the end of the function to see the quote template.
@ -182,9 +182,14 @@ pub(crate) fn bridge_fn(
let name_with_prefix = format_ident!("node_{}", name);
let name_without_prefix = Ident::new(name, Span::call_site());
let ts_signature_comment = generate_ts_signature_comment(name, sig, bridging_kind);
let input_args = extract_arg_names_and_types(sig)?;
let ts_metadata = generate_ts_metadata(
name,
sig.asyncness.is_some(),
&input_args,
result_type(&sig.output),
bridging_kind,
);
let body = match (sig.asyncness, bridging_kind) {
(Some(_), _) => bridge_fn_async_body(&sig.ident, name, bridging_kind, &input_args),
@ -200,51 +205,83 @@ pub(crate) fn bridge_fn(
Ok(quote! {
#[cfg(feature = "node")]
#[allow(non_snake_case)]
#[doc = #ts_signature_comment]
pub fn #name_with_prefix(
mut cx: node::FunctionContext,
) -> node::JsResult<node::JsValue> {
#body
}
#[cfg(all(feature = "metadata", feature = "node"))]
#ts_metadata
#[cfg(feature = "node")]
node_register!(#name_without_prefix);
})
}
/// Generates a string, containing the *Rust* signature of a bridged function, that gen_ts_decl.py
/// can use to generate Native.d.ts.
fn generate_ts_signature_comment(
/// Generates the code to embed `libsignal_bridge_types::metadata` metadata
fn generate_ts_metadata(
name_without_prefix: &str,
sig: &Signature,
asyncness: bool,
input_args: &[(&Ident, &Type)],
result_type: TokenStream2,
bridging_kind: &BridgingKind,
) -> String {
let mut ts_args = vec![];
) -> TokenStream2 {
let krate = crates::libsignal_bridge_types();
let mut input_args: Vec<_> = input_args
.iter()
.map(|(name, ty)| (name.to_string(), ty.to_token_stream()))
.collect();
match bridging_kind {
BridgingKind::Regular => {}
BridgingKind::Io { runtime } => {
ts_args.push(format!("async_runtime: &{}", runtime.to_token_stream()))
let runtime = runtime.to_token_stream();
input_args.insert(0, ("async_runtime".to_string(), quote!(&#runtime)))
}
}
ts_args.extend(
sig.inputs
.iter()
.map(|arg| arg.to_token_stream().to_string().replace('\n', " ")),
);
let result_type_format = match (sig.asyncness, bridging_kind) {
(Some(_), BridgingKind::Io { .. }) => |ty| format!("CancellablePromise<{ty}>"),
(Some(_), _) => |ty| format!("Promise<{ty}>"),
(None, _) => |ty| format!("{ty}"),
let argument_names = input_args
.iter()
.map(|(x, _)| to_lower_camel_case_preserve_underscores(x))
.collect_vec();
let argument_types = input_args.iter().map(|(_, x)| x).collect_vec();
let return_type_format = match (asyncness, bridging_kind) {
(true, BridgingKind::Io { .. }) => "CancellablePromise<{return_type}>",
(true, _) => "Promise<{return_type}>",
(false, _) => "{return_type}",
};
let result_type_str = result_type_format(result_type(&sig.output));
let md = quote!(#krate::metadata);
let metadata_name = format_ident!("_BRIDGE_NODE_METADATA_{name_without_prefix}");
let type_info_trait = if asyncness {
quote!(AsyncArgTypeInfo)
} else {
quote!(ArgTypeInfo)
};
quote! {
#[#md::linkme::distributed_slice(#md::node::NODE_ITEMS)]
#[linkme(crate = #md::linkme)]
static #metadata_name: #md::FnWithModule<#md::node::TsMetadataContext> = #md::FnWithModule {
module_path: module_path!(),
apply: |ctx| {
use #md::node::result_type_helper::*;
let return_type: ResultMetadataTransformHelper<#result_type> = Default::default();
let return_type = return_type.register_ts_ffi_type(ctx);
let mut arguments = Vec::new();
#(arguments.push((
#argument_names.into(),
<#argument_types as #krate::node::#type_info_trait>::register_ts_ffi_type(ctx)
));)*
ctx.native_functions.insert(
#name_without_prefix.into(),
#md::node::NativeFunction { arguments, return_type: format!(#return_type_format) },
);
},
};
}
}
format!(
"ts: `export function {}({}): {}`",
name_without_prefix,
ts_args.join(", "),
result_type_str
)
fn to_lower_camel_case_preserve_underscores(x: &str) -> String {
let x_sans_underscore = x.trim_start_matches('_');
let core = x_sans_underscore.to_lower_camel_case();
format!("{}{core}", &x[0..(x.len() - x_sans_underscore.len())])
}
pub(crate) fn name_from_ident(ident: &Ident) -> String {
@ -259,23 +296,20 @@ pub(crate) fn name_from_ident(ident: &Ident) -> String {
pub(crate) fn bridge_trait(trait_to_bridge: &ItemTrait, js_name: &str) -> Result<TokenStream2> {
let trait_name = &trait_to_bridge.ident;
let wrapper_name = format_ident!("Node{}", trait_to_bridge.ident);
let krate = crates::libsignal_bridge_types();
let callbacks = trait_to_bridge
.items
.iter()
.map(bridge_callback_item)
.map(|x| bridge_callback_item(x, &krate))
.collect::<Result<Vec<_>>>()?;
let callback_impls = callbacks.iter().map(|c| &c.implementation);
let callback_ts_decls = callbacks.iter().map(|c| &c.ts_decl);
let ts_declaration_comment = format!(
"ts: `export /*trait*/ type {js_name} = {{\n{}\n}};`",
callback_ts_decls.format("\n")
);
let callback_bridge_trait_functions = callbacks.iter().map(|c| &c.bridge_trait_function);
let md = quote!(#krate::metadata);
let metadata_name = format_ident!("_BRIDGE_NODE_METADATA_{trait_name}");
Ok(quote! {
#[cfg(feature = "node")]
#[doc = #ts_declaration_comment]
pub struct #wrapper_name(node::RootAndChannel);
#[cfg(feature = "node")]
@ -299,15 +333,29 @@ pub(crate) fn bridge_trait(trait_to_bridge: &ItemTrait, js_name: &str) -> Result
impl #trait_name for #wrapper_name {
#(#callback_impls)*
}
#[cfg(all(feature = "node", feature = "metadata"))]
#[#md::linkme::distributed_slice(#md::node::NODE_ITEMS)]
#[linkme(crate = #md::linkme)]
static #metadata_name: #md::FnWithModule<#md::node::TsMetadataContext> = #md::FnWithModule {
module_path: module_path!(),
apply: |ctx| {
let mut functions = Vec::new();
#(#callback_bridge_trait_functions)*
ctx.bridge_traits.insert(#js_name.to_string(), functions);
},
};
})
}
struct Callback {
implementation: TokenStream2,
ts_decl: String,
/// Push a `node::BridgeTraitFunction` onto the local `functions` Vec
/// `ctx: &mut TsMetadataContext` is in scope
bridge_trait_function: TokenStream2,
}
fn bridge_callback_item(item: &TraitItem) -> Result<Callback> {
fn bridge_callback_item(item: &TraitItem, krate: &TokenStream2) -> Result<Callback> {
let TraitItem::Fn(item) = item else {
return Err(Error::new(item.span(), "only fns are supported"));
};
@ -395,21 +443,35 @@ fn bridge_callback_item(item: &TraitItem) -> Result<Callback> {
}
};
// operation(foo: number): void;
let js_arg_decls = item.sig.inputs.iter().filter_map(|arg| match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(arg) => {
let Pat::Ident(arg_name) = &*arg.pat else {
// Diagnosed elsewhere.
return None;
};
Some(format!("{}: {}", arg_name.ident, arg.ty.to_token_stream()))
}
});
let args = item
.sig
.inputs
.iter()
.filter_map(|arg| match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(arg) => {
let Pat::Ident(arg_name) = &*arg.pat else {
// Diagnosed elsewhere.
return None;
};
Some((&arg_name.ident, &arg.ty))
}
})
.collect_vec();
let arg_names = args
.iter()
.map(|(x, _)| to_lower_camel_case_preserve_underscores(&x.to_string()))
.collect_vec();
let arg_types = args.iter().map(|(_, x)| x).collect_vec();
let result_ty = result_type(&sig.output);
let result_string = if sig.asyncness.is_some() {
let result_ty = result_type(&sig.output);
format!("Promise<{result_ty}>")
let return_type = if sig.asyncness.is_some() {
quote! {{
use #krate::metadata::node::result_type_helper::*;
let return_type: CallbackResultMetadataTransformHelper<#result_ty> = Default::default();
let return_type = return_type.register_ts_ffi_type(ctx);
format!("Promise<{return_type}>")
}}
} else {
if !matches!(sig.output, ReturnType::Default) {
return Err(Error::new(
@ -417,17 +479,25 @@ fn bridge_callback_item(item: &TraitItem) -> Result<Callback> {
"non-async callbacks with results are not supported for Node",
));
}
"void".to_owned()
quote!("void".to_string())
};
let ts_decl = format!(
"{}({}): {};",
js_operation_name,
js_arg_decls.format(", "),
result_string
);
Ok(Callback {
implementation,
ts_decl,
bridge_trait_function: quote! {
let mut arguments = Vec::new();
#(arguments.push((
#arg_names.to_string(),
<#arg_types as #krate::node::ResultTypeInfo>::register_ts_ffi_type(ctx),
));)*
let return_type = #return_type;
functions.push(#krate::metadata::node::BridgeTraitFunction {
name: #js_operation_name.to_string(),
body: #krate::metadata::node::NativeFunction {
arguments,
return_type,
},
});
},
})
}

View File

@ -9,6 +9,20 @@ use syn::spanned::Spanned;
use syn::*;
use syn_mid::{FnArg, Pat, PatType, Signature};
pub(crate) mod crates {
use super::*;
fn pkg_name() -> String {
std::env::var("CARGO_PKG_NAME").expect("Missing CARGO_PKG_NAME")
}
pub(crate) fn libsignal_bridge_types() -> TokenStream2 {
if pkg_name() == "libsignal-bridge-types" {
quote!(crate)
} else {
quote!(::libsignal_bridge_types)
}
}
}
/// Returns the tokens of the type in `output_as_written`, or `()` if no return type was written.
pub(crate) fn result_type(output_as_written: &ReturnType) -> TokenStream2 {
match output_as_written {

View File

@ -125,6 +125,27 @@ async fn UnauthenticatedChatConnection_send(
.await
}
#[bridge_io(TokioAsyncContext)]
async fn UnauthenticatedChatConnection_send_raw_grpc(
chat: &UnauthenticatedChatConnection,
service: String,
method: String,
payload: Box<[u8]>,
) -> Result<Vec<u8>, RequestError<Infallible>> {
chat.as_typed(|chat| {
Box::pin(libsignal_net_chat::grpc::raw_grpc(
"unauth",
chat.0
.shared_h2_connection()
.expect("requires an H2 connection"),
&service,
&method,
payload.into_vec(),
))
})
.await
}
#[bridge_io(TokioAsyncContext)]
async fn UnauthenticatedChatConnection_disconnect(chat: &UnauthenticatedChatConnection) {
chat.disconnect().await
@ -306,6 +327,27 @@ async fn AuthenticatedChatConnection_send(
.await
}
#[bridge_io(TokioAsyncContext)]
async fn AuthenticatedChatConnection_send_raw_grpc(
chat: &AuthenticatedChatConnection,
service: String,
method: String,
payload: Box<[u8]>,
) -> Result<Vec<u8>, RequestError<Infallible>> {
chat.as_typed(|chat| {
Box::pin(libsignal_net_chat::grpc::raw_grpc(
"auth",
chat.0
.shared_h2_connection()
.expect("requires an H2 connection"),
&service,
&method,
payload.into_vec(),
))
})
.await
}
#[bridge_io(TokioAsyncContext)]
async fn AuthenticatedChatConnection_disconnect(chat: &AuthenticatedChatConnection) {
chat.disconnect().await

View File

@ -14,8 +14,8 @@ use libsignal_core::{Aci, E164};
use libsignal_keytrans::{AccountData, StoredAccountData};
use libsignal_net_chat::api::RequestError;
use libsignal_net_chat::api::keytrans::{
CheckMode, Error, KeyTransparencyClient, MaybePartial, SearchKey, TreeHeadWithTimestamp,
UsernameHash, check,
AccountDataField, AccountDataFieldReset as _, CheckMode, Error, KeyTransparencyClient,
MaybePartial, SearchKey, TreeHeadWithTimestamp, UsernameHash, check,
};
use libsignal_protocol::PublicKey;
use prost::{DecodeError, Message};
@ -38,6 +38,20 @@ fn KeyTransparency_UsernameHashSearchKey(hash: &[u8]) -> Vec<u8> {
UsernameHash::from_slice(hash).as_search_key()
}
#[bridge_fn]
fn KeyTransparency_ResetDataField(
account_data: Box<[u8]>,
field: AsType<AccountDataField, u8>,
) -> Vec<u8> {
// The only failure is decoding error, we'll use empty vec for that.
let decoded: Result<StoredAccountData, _> = try_decode(account_data);
let Ok(account_data) = decoded else {
log::warn!("Failed to decode stored account data");
return vec![];
};
account_data.reset(field.into_inner()).encode_to_vec()
}
#[bridge_io(TokioAsyncContext)]
#[expect(clippy::too_many_arguments)]
async fn KeyTransparency_Check(

View File

@ -372,20 +372,6 @@ fn SignalMessage_New(
)
}
#[bridge_fn(ffi = "message_verify_mac")]
fn SignalMessage_VerifyMac(
msg: &SignalMessage,
sender_identity_key: &PublicKey,
receiver_identity_key: &PublicKey,
mac_key: &[u8],
) -> Result<bool> {
msg.verify_mac(
&IdentityKey::new(*sender_identity_key),
&IdentityKey::new(*receiver_identity_key),
mac_key,
)
}
#[bridge_fn(ffi = "message_get_sender_ratchet_key", node = false)]
fn SignalMessage_GetSenderRatchetKey(m: &SignalMessage) -> PublicKey {
*m.sender_ratchet_key()
@ -974,8 +960,35 @@ fn SessionRecord_ArchiveCurrentState(session_record: &mut SessionRecord) -> Resu
}
#[bridge_fn]
fn SessionRecord_HasUsableSenderChain(s: &SessionRecord, now: Timestamp) -> Result<bool> {
s.has_usable_sender_chain(now.into(), SessionUsabilityRequirements::NotStale)
fn SessionRecord_HasUsableSenderChain(
s: &SessionRecord,
require_pq_ratio: f64,
now: Timestamp,
) -> Result<bool> {
let has_chain =
s.has_usable_sender_chain(now.into(), SessionUsabilityRequirements::NotStale)?;
if !has_chain {
return Ok(false);
}
let has_pq_chain = s.has_usable_sender_chain(
now.into(),
SessionUsabilityRequirements::NotStale
| SessionUsabilityRequirements::EstablishedWithPqxdh
| SessionUsabilityRequirements::Spqr,
)?;
if has_pq_chain || require_pq_ratio == 0.0 {
return Ok(true);
}
let require_pq_ratio = if require_pq_ratio > 1.0 {
log::warn!("pinning overly high PQ ratio {require_pq_ratio} to 1.0");
1.0
} else if require_pq_ratio < 0.0 {
log::warn!("pinning overly low PQ ratio {require_pq_ratio} to 0.0");
0.0
} else {
require_pq_ratio
};
Ok(should_use_nonpq_session(require_pq_ratio, s.alice_base_key().expect("we should have a current session, since has_usable_sender_chain returned a non-error value")))
}
#[bridge_fn]

View File

@ -24,6 +24,7 @@ libsignal-keytrans = { workspace = true }
libsignal-message-backup = { workspace = true, features = ["json"] }
libsignal-net = { workspace = true }
libsignal-net-chat = { workspace = true }
libsignal-net-grpc = { workspace = true, features = ["json"] }
libsignal-protocol = { workspace = true }
zkgroup = { workspace = true }
@ -54,3 +55,4 @@ ffi = ["libsignal-bridge-types/ffi"]
jni = ["dep:jni", "libsignal-bridge-types/jni"]
node = ["dep:linkme", "dep:neon", "libsignal-bridge-types/node"]
signal-media = ["libsignal-bridge-types/signal-media"]
metadata = ["libsignal-bridge-types/metadata"]

View File

@ -73,13 +73,18 @@ async fn TESTING_FakeChatServer_GetNextRemote(server: &FakeChatServer) -> FakeCh
fn TESTING_FakeChatConnection_Create(
tokio: &TokioAsyncContext,
listener: Box<dyn ChatListener>,
grpc_overrides_joined_by_newlines: String,
alerts_joined_by_newlines: String,
) -> FakeChatConnection {
// "".split_terminator(...) produces [], while normal split() produces [""].
// Leaking is unfortunate, but more expedient than mapping to remote config keys or similar.
let grpc_overrides = String::leak(grpc_overrides_joined_by_newlines).split_terminator('\n');
let alerts = alerts_joined_by_newlines.split_terminator('\n');
let (chat, remote) = libsignal_bridge_types::net::chat::FakeChatConnection::new(
tokio.handle(),
listener.into_event_listener(),
grpc_overrides,
alerts,
);
FakeChatConnection {
@ -96,7 +101,8 @@ fn TESTING_FakeChatConnection_CreateProvisioning(
let (chat, remote) = libsignal_bridge_types::net::chat::FakeChatConnection::new(
tokio.handle(),
listener.into_event_listener(),
vec![],
[],
[],
);
FakeChatConnection {
chat: Some(chat).into(),
@ -159,6 +165,37 @@ fn TESTING_FakeChatRemoteEnd_SendServerResponse(
.expect("chat task finished")
}
#[bridge_io(TokioAsyncContext)]
async fn TESTING_FakeChatRemoteEnd_SendServerGrpcResponse(
chat: &FakeChatRemoteEnd,
response: &FakeChatResponse,
) {
let FakeChatResponse(ResponseProto {
id,
status,
message,
headers,
body,
}) = response;
assert!(
message.as_deref().unwrap_or_default().is_empty(),
"messages not supported for gRPC"
);
assert!(headers.is_empty(), "headers not yet implemented for gRPC");
let http_response = http::Response::builder()
.status(u16::try_from(status.unwrap_or_default()).unwrap_or(u16::MAX))
.body(body.as_ref().cloned().unwrap_or_default())
.expect("valid");
chat.0
.grpc()
.await
.send_response(id.unwrap_or_default(), http_response)
.expect("chat task finished");
}
#[bridge_fn]
fn TESTING_FakeChatRemoteEnd_InjectConnectionInterrupted(chat: &FakeChatRemoteEnd) {
chat.0
@ -203,6 +240,40 @@ async fn TESTING_FakeChatRemoteEnd_ReceiveIncomingRequest(
Some((http_request, id.unwrap()))
}
#[bridge_io(TokioAsyncContext)]
async fn TESTING_FakeChatRemoteEnd_ReceiveIncomingGrpcRequest(
chat: &FakeChatRemoteEnd,
) -> Option<(HttpRequest, u64)> {
let (id, request) = chat
.0
.grpc()
.await
.receive_request()
.await
.expect("message was invalid")?;
let (
http::request::Parts {
method,
uri,
headers,
..
},
body,
) = request.into_parts();
let http_request = HttpRequest {
method,
path: uri
.into_parts()
.path_and_query
.unwrap_or(http::uri::PathAndQuery::from_static("")),
body: Some(body),
headers: headers.into(),
};
Some((http_request, id))
}
#[bridge_fn]
fn TESTING_ChatResponseConvert(body_present: bool) -> ChatResponse {
let body = match body_present {
@ -279,6 +350,52 @@ fn TESTING_FakeChatResponse_Create(
})
}
#[bridge_fn]
fn TESTING_FakeChatRemoteEnd_NextGrpcMessage(input: &[u8], offset: u32) -> (u32, u32) {
// Taking an offset avoids extra copies in the streaming input case.
let input = &input[offset.try_into().expect("valid offset for buffer")..];
let message_slice = libsignal_net_grpc::expect_next_grpc_message_for_testing(input);
// We return a (start, end) pair for the app language to slice.
// Unfortunately, getting that back out takes a bit of work.
let message_offset = if let Some(first_elem) = message_slice.first() {
// TODO: replace with slice::element_offset at MSRV 1.94.
let first_elem = std::ptr::from_ref(first_elem);
let slice_range = input.as_ptr_range();
assert!(
slice_range.contains(&first_elem),
"result should be a subslice"
);
// Note: subtracting raw addresses only works because the elements are bytes.
first_elem.addr() - slice_range.start.addr()
} else {
// If the message is empty, the header must have been the entire rest of the input.
input.len()
};
let full_offset = offset + u32::try_from(message_offset).expect("input will never be >1GB");
(
full_offset,
full_offset + u32::try_from(message_slice.len()).expect("input will never be >1GB"),
)
}
#[bridge_fn]
fn TESTING_FakeChatRemoteEnd_GrpcFrameForMessageLength(len: u32) -> Vec<u8> {
let mut result = Vec::with_capacity(5);
result.push(0);
result.extend_from_slice(&len.to_be_bytes());
result
}
#[bridge_fn]
fn TESTING_FakeChatRemoteEnd_BinprotoToJson(name: String, input: &[u8]) -> String {
libsignal_net_grpc::json::expect_binproto_to_json_by_name(&name, input)
}
#[bridge_fn]
fn TESTING_FakeChatRemoteEnd_JsonToBinproto(name: String, input: String) -> Vec<u8> {
libsignal_net_grpc::json::expect_json_to_binproto_by_name(&name, &input)
}
make_error_testing_enum! {
enum TestingChatConnectError for ConnectError {
WebSocket => WebSocketConnectionFailed,

View File

@ -3,8 +3,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
use libsignal_keytrans::{StoredAccountData, StoredMonitoringData};
use libsignal_net_chat::api::RequestError;
use libsignal_net_chat::api::keytrans::Error as KeyTransError;
use prost::Message;
use crate::*;
@ -30,3 +32,23 @@ fn TESTING_KeyTransNonFatalVerificationFailure() -> Result<(), RequestError<KeyT
fn TESTING_KeyTransChatSendError() -> Result<(), RequestError<KeyTransError>> {
Err(RequestError::Timeout)
}
#[bridge_fn]
fn TESTING_KeyTransStoredAccountData() -> Vec<u8> {
StoredAccountData {
aci: Some(StoredMonitoringData {
pos: 1,
..Default::default()
}),
e164: Some(StoredMonitoringData {
pos: 2,
..Default::default()
}),
username_hash: Some(StoredMonitoringData {
pos: 3,
..Default::default()
}),
last_tree_head: None,
}
.encode_to_vec()
}

View File

@ -82,7 +82,7 @@ impl ConnectUnauthChat for ConnectFakeChat {
| libsignal_net::chat::ws::ListenerEvent::ReceivedMessage(_, _) => (),
};
let (chat, remote) = ChatConnection::new_fake(self.0.clone(), Box::new(listener), []);
let (chat, remote) = ChatConnection::new_fake(self.0.clone(), Box::new(listener), [], []);
std::future::ready(
self.1

View File

@ -105,6 +105,10 @@ impl<'storage, 'context: 'storage> node::ArgTypeInfo<'storage, 'context> for Nee
fn load_from(_stored: &'storage mut Self::StoredType) -> Self {
Self::None
}
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
"null".into()
}
}
#[cfg(feature = "node")]
@ -123,6 +127,10 @@ impl<'storage> node::AsyncArgTypeInfo<'storage> for NeedsCleanup {
// We only want to test that the storage is cleaned up, not the value passed into the wrapped function.
Self::None
}
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
"null".into()
}
}
/// A type that implements ArgTypeInfo but always produces an error when "borrowed" from the
@ -162,6 +170,10 @@ impl node::SimpleArgTypeInfo for ErrorOnBorrow {
) -> node::NeonResult<Self> {
node::Context::throw_type_error(cx, "deliberate error")
}
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
"null".into()
}
}
/// A type that implements ArgTypeInfo but panics as it is "borrowed" from the app-provided
@ -199,6 +211,11 @@ impl node::SimpleArgTypeInfo for PanicOnBorrow {
) -> node::NeonResult<Self> {
panic!("deliberate panic")
}
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
"null".into()
}
}
/// A type that implements ArgTypeInfo but panics on the secondary "load" step after the "borrow"
@ -258,6 +275,11 @@ impl<'storage, 'context: 'storage> node::ArgTypeInfo<'storage, 'context> for Pan
fn load_from(_stored: &'storage mut Self::StoredType) -> Self {
panic!("deliberate panic")
}
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
"null".into()
}
}
#[cfg(feature = "node")]
@ -276,6 +298,11 @@ impl<'storage> node::AsyncArgTypeInfo<'storage> for PanicOnLoad {
fn load_async_arg(_stored: &'storage mut Self::StoredType) -> Self {
panic!("deliberate panic")
}
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
"null".into()
}
}
/// A type that implements ResultTypeInfo but always fails to produce a result.
@ -311,6 +338,11 @@ impl<'a> node::ResultTypeInfo<'a> for ErrorOnReturn {
fn convert_into(self, cx: &mut impl node::Context<'a>) -> node::JsResult<'a, Self::ResultType> {
cx.throw_type_error("deliberate error")
}
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
"null".into()
}
}
/// A type that implements ResultTypeInfo but always panics when producing a result.
@ -347,6 +379,11 @@ impl<'a> node::ResultTypeInfo<'a> for PanicOnReturn {
) -> node::JsResult<'a, Self::ResultType> {
panic!("deliberate panic");
}
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
"null".into()
}
}
#[derive(derive_more::Deref)]
@ -430,6 +467,11 @@ impl<'storage> node::AsyncArgTypeInfo<'storage> for TestingFutureCancellationGua
fn load_async_arg(stored: &'storage mut Self::StoredType) -> Self {
stored.take().unwrap().0
}
#[cfg(feature = "metadata")]
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
"TestingFutureCancellationGuard".into()
}
}
bridge_as_handle!(TestingFutureCancellationCounter);

View File

@ -84,6 +84,7 @@ jni-type-tagging = []
jni-invoke-annotated = []
extra-jni-checks = ["jni-type-tagging", "jni-invoke-annotated"]
node = ["neon", "linkme", "signal-neon-futures"]
metadata = ["linkme", "serde/derive"]
[target.'cfg(not(any(windows, target_arch = "x86")))'.dependencies]
# sha2's asm implementation uses standalone .S files that aren't compiled correctly on Windows,

View File

@ -563,7 +563,7 @@ impl SimpleArgTypeInfo for &libsignal_account_keys::BackupKey {
}
macro_rules! bridge_trait {
($name:ident) => {
($name:ident, $load:expr) => {
paste! {
impl<'a> ArgTypeInfo<'a> for &'a mut dyn $name {
type ArgType = crate::ffi::ConstPointer< [<Ffi $name Struct >] >;
@ -576,11 +576,14 @@ macro_rules! bridge_trait {
}
}
fn load_from(stored: &'a mut Self::StoredType) -> Self {
stored
($load)(stored)
}
}
}
};
($name:ident) => {
bridge_trait!($name, std::convert::identity);
};
}
bridge_trait!(IdentityKeyStore);
@ -589,8 +592,8 @@ bridge_trait!(SenderKeyStore);
bridge_trait!(SessionStore);
bridge_trait!(SignedPreKeyStore);
bridge_trait!(KyberPreKeyStore);
bridge_trait!(InputStream);
bridge_trait!(SyncInputStream);
bridge_trait!(InputStream, |x: &'a mut Self::StoredType| &mut x.0);
bridge_trait!(SyncInputStream, |x: &'a mut Self::StoredType| &mut x.0);
impl<'a> ArgTypeInfo<'a> for Box<dyn ChatListener> {
type ArgType = crate::ffi::ConstPointer<FfiChatListenerStruct>;
@ -1426,6 +1429,7 @@ trivial!(u64);
trivial!(i64);
trivial!(usize);
trivial!(bool);
trivial!(f64);
/// Syntactically translates `bridge_fn` argument types (and callback result types) to FFI types for
/// `cbindgen`.
@ -1441,6 +1445,7 @@ macro_rules! ffi_arg_type {
(i32) => (i32);
(u32) => (u32);
(u64) => (u64);
(f64) => (f64);
(Option<u32>) => (u32);
(usize) => (usize);
(bool) => (bool);
@ -1546,6 +1551,7 @@ macro_rules! ffi_result_type {
(Option<u32>) => (u32);
(u64) => (u64);
(i64) => (i64);
(f64) => (f64);
(Option<u64>) => (u64);
(bool) => (bool);
(&str) => (*const std::ffi::c_char);

View File

@ -1,43 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::io;
use async_trait::async_trait;
use libsignal_bridge_macros::bridge_callbacks;
use crate::ffi;
use crate::io::{InputStream, InputStreamRead, SyncInputStream};
use crate::support::{BridgedCallbacks, ResultLike, WithContext};
#[bridge_callbacks(ffi = "FfiInputStream", jni = false, node = false)]
trait BridgeInputStream {
fn read(&self, buf: &mut [u8]) -> Result<usize, io::Error>;
fn skip(&self, amount: u64) -> Result<(), io::Error>;
}
pub type FfiSyncInputStreamStruct = FfiInputStreamStruct;
#[async_trait(?Send)]
impl<T: BridgeInputStream> InputStream for BridgedCallbacks<T> {
fn read<'out, 'a: 'out>(&'a self, buf: &mut [u8]) -> io::Result<InputStreamRead<'out>> {
let amount_read = self.0.read(buf)?;
Ok(InputStreamRead::Ready { amount_read })
}
async fn skip(&self, amount: u64) -> io::Result<()> {
self.0.skip(amount)
}
}
impl<T: BridgeInputStream> SyncInputStream for BridgedCallbacks<T> {
fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
fn skip(&self, amount: u64) -> io::Result<()> {
self.0.skip(amount)
}
}

View File

@ -8,6 +8,8 @@ use std::ffi::CString;
use derive_where::derive_where;
use libsignal_protocol::*;
use crate::support::describe_panic;
#[macro_use]
mod convert;
pub use convert::*;
@ -21,16 +23,15 @@ pub use error::*;
mod futures;
pub use futures::*;
mod io;
pub use io::*;
// TODO: This re-export is because of the ffi_arg_type macro expecting all bridging structs to be
// TODO: These re-exports are because of the ffi_arg_type macro expecting all bridging structs to be
// under the ffi module; eventually we should be able to remove it.
pub use crate::io::FfiSyncInputStreamStruct;
pub use crate::protocol::storage::{
FfiIdentityKeyStoreStruct, FfiKyberPreKeyStoreStruct, FfiPreKeyStoreStruct,
FfiSenderKeyStoreStruct, FfiSessionStoreStruct, FfiSignedPreKeyStoreStruct,
};
use crate::support::describe_panic;
pub type FfiInputStreamStruct = FfiSyncInputStreamStruct;
#[derive(Debug)]
pub struct NullPointerError;

View File

@ -10,8 +10,12 @@ use std::task::{Context, Poll};
use async_trait::async_trait;
use futures_util::future::LocalBoxFuture;
use futures_util::{AsyncRead, FutureExt as _};
use libsignal_bridge_macros::bridge_callbacks;
use mediasan_common::{AsyncSkip, Skip};
use crate::support::{ResultLike, WithContext};
use crate::*;
/// The result of a [`InputStream::read`].
///
/// This `enum` exists so that [`InputStream::read`] may be implemented as either a sync or async operation. In the
@ -61,6 +65,7 @@ pub trait InputStream {
}
/// An input stream of bytes.
#[bridge_callbacks(jni = false, node = false)]
pub trait SyncInputStream {
/// Read an amount of bytes from the input stream.
///
@ -74,7 +79,7 @@ pub trait SyncInputStream {
/// # Errors
///
/// If an I/O error occurred while reading from the input, an [`io::Error`] is returned.
fn read(&self, buf: &mut [u8]) -> io::Result<usize>;
fn read(&self, buf: &mut [u8]) -> Result<usize, io::Error>;
/// Skip an amount of bytes in the input stream.
///
@ -82,7 +87,20 @@ pub trait SyncInputStream {
///
/// If the requested number of bytes could not be skipped for any reason, including if the end of stream was
/// reached, an error must be returned.
fn skip(&self, amount: u64) -> io::Result<()>;
fn skip(&self, amount: u64) -> Result<(), io::Error>;
}
/// Any SyncInputStream is a valid "async" InputStream.
#[async_trait(?Send)]
impl<T: SyncInputStream> InputStream for T {
fn read<'out, 'a: 'out>(&'a self, buf: &mut [u8]) -> io::Result<InputStreamRead<'out>> {
let amount_read = self.read(buf)?;
Ok(InputStreamRead::Ready { amount_read })
}
async fn skip(&self, amount: u64) -> io::Result<()> {
self.skip(amount)
}
}
pub struct SyncInput<'a> {

View File

@ -239,6 +239,13 @@ impl SimpleArgTypeInfo<'_> for u64 {
}
}
impl SimpleArgTypeInfo<'_> for f64 {
type ArgType = jdouble;
fn convert_from(_env: &mut JNIEnv, foreign: &jdouble) -> Result<Self, BridgeLayerError> {
Ok(*foreign)
}
}
/// Supports values `0..=Long.MAX_VALUE`.
///
/// Negative `long` values are *not* reinterpreted as large `u64` values.
@ -2677,6 +2684,9 @@ macro_rules! jni_arg_type {
(u64) => {
::jni::sys::jlong
};
(f64) => {
::jni::sys::jdouble
};
(bool) => {
::jni::sys::jboolean
};

View File

@ -6,15 +6,13 @@
use std::cell::RefCell;
use std::io;
use async_trait::async_trait;
use super::*;
use crate::io::{InputStream, InputStreamRead, SyncInputStream};
use crate::io::SyncInputStream;
pub type JavaInputStream<'a> = JObject<'a>;
pub type JavaSyncInputStream<'a> = JObject<'a>;
/// Implementation of [`InputStream`] for an argument to a bridge function.
/// Implementation of [`InputStream`](crate::io::InputStream) for an argument to a bridge function.
pub struct JniBridgeInputStream<'a> {
env: RefCell<EnvHandle<'a>>,
stream: &'a JObject<'a>,
@ -111,19 +109,6 @@ impl From<BridgeOrIoError> for IoError {
}
}
#[async_trait(?Send)]
impl InputStream for JniBridgeInputStream<'_> {
fn read<'out, 'a: 'out>(&'a self, buf: &mut [u8]) -> io::Result<InputStreamRead<'out>> {
let amount_read = self.do_read(buf)?;
Ok(InputStreamRead::Ready { amount_read })
}
async fn skip(&self, amount: u64) -> io::Result<()> {
self.do_skip(amount)?;
Ok(())
}
}
impl SyncInputStream for JniBridgeInputStream<'_> {
fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
Ok(self.do_read(buf)?)

View File

@ -20,7 +20,7 @@ pub use jni::objects::{
JValueOwned, ReleaseMode,
};
use jni::objects::{GlobalRef, JThrowable};
pub use jni::sys::{jboolean, jint, jlong};
pub use jni::sys::{jboolean, jdouble, jint, jlong};
use libsignal_account_keys::Error as PinError;
use libsignal_core::try_scoped;
use libsignal_net::chat::{ConnectError as ChatConnectError, SendError as ChatSendError};

View File

@ -6,6 +6,9 @@
#![allow(clippy::missing_safety_doc)]
#![deny(clippy::unwrap_used)]
#[cfg(feature = "metadata")]
pub mod metadata;
#[cfg(feature = "ffi")]
#[macro_use]
pub mod ffi;

View File

@ -0,0 +1,108 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! This module provides metadata about the bridge layer which will be consumed downstream for
//! various purposes:
//!
//! - To emit `Native.ts`, see `libsignal-node-native_ts`
//!
//! While some metadata facilities are shared, they're specialized to each client language.
// This is pub so that it can be used in bridge macros.
pub use linkme;
use linkme::distributed_slice;
use serde::Serialize;
#[cfg(feature = "node")]
pub mod node {
use std::collections::{BTreeMap, BTreeSet};
use super::*;
#[derive(Debug, Clone, Serialize, Default)]
pub struct TsMetadataContext {
pub opaque_types: BTreeSet<String>,
pub native_functions: BTreeMap<String, NativeFunction>,
pub bridge_traits: BTreeMap<String, Vec<BridgeTraitFunction>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct NativeFunction {
/// (name, type)
pub arguments: Vec<(String, String)>,
pub return_type: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct BridgeTraitFunction {
pub name: String,
pub body: NativeFunction,
}
/// These functions should mutate the attached [TsMetadataContext] to register their item.
#[distributed_slice]
pub static NODE_ITEMS: [FnWithModule<TsMetadataContext>];
/// See [crate::support]'s `transform_helper` for how this works, and the rationale.
///
/// These functions provide the metadata-side (`register_ts_ffi_type()`) of `.ok_if_needed()`
///
/// ```
/// # use libsignal_bridge_types::metadata::node::result_type_helper::*;
/// let x: ResultMetadataTransformHelper<i32> = Default::default();
/// assert_eq!(x.register_ts_ffi_type(&mut Default::default()).as_str(), "number");
/// let y: ResultMetadataTransformHelper<Result<i32, String>> = Default::default();
/// assert_eq!(y.register_ts_ffi_type(&mut Default::default()).as_str(), "number");
/// ```
pub mod result_type_helper {
use std::marker::PhantomData;
use derive_where::derive_where;
use crate::metadata::node::TsMetadataContext;
use crate::node::{CallbackResultTypeInfo, ResultTypeInfo};
#[derive_where(Default)]
pub struct ResultMetadataTransformHelper<T>(PhantomData<T>);
impl<'a, T: ResultTypeInfo<'a>> ResultMetadataTransformHelper<T> {
pub fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
T::register_ts_ffi_type(ctx)
}
}
pub trait ResultMetadataTransformHelperTrait {
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String;
}
impl<'a, T: ResultTypeInfo<'a>, E> ResultMetadataTransformHelperTrait
for ResultMetadataTransformHelper<Result<T, E>>
{
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
T::register_ts_ffi_type(ctx)
}
}
#[derive_where(Default)]
pub struct CallbackResultMetadataTransformHelper<T>(PhantomData<T>);
impl<T: CallbackResultTypeInfo> CallbackResultMetadataTransformHelper<T> {
pub fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
T::register_ts_ffi_type(ctx)
}
}
pub trait CallbackResultMetadataTransformHelperTrait {
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String;
}
impl<T: CallbackResultTypeInfo, E> CallbackResultMetadataTransformHelperTrait
for CallbackResultMetadataTransformHelper<Result<T, E>>
{
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
T::register_ts_ffi_type(ctx)
}
}
}
}
pub struct FnWithModule<Ctx> {
/// The module the function is defined in
pub module_path: &'static str,
pub apply: fn(&mut Ctx),
}

View File

@ -423,9 +423,11 @@ impl FakeChatConnection {
pub fn new<'a>(
tokio_runtime: tokio::runtime::Handle,
listener: chat::ws::EventListener,
grpc_overrides: impl IntoIterator<Item = &'static str>,
alerts: impl IntoIterator<Item = &'a str>,
) -> (Self, FakeChatRemote) {
let (inner, remote) = ChatConnection::new_fake(tokio_runtime, listener, alerts);
let (inner, remote) =
ChatConnection::new_fake(tokio_runtime, listener, grpc_overrides, alerts);
(Self(inner), remote)
}
@ -533,8 +535,10 @@ async fn establish_chat_connection(
proxy_mode,
) {
(None, DirectOrProxyModeDiscriminants::DirectOnly)
| (None, DirectOrProxyModeDiscriminants::DirectThenProxy)
| (Some(_), DirectOrProxyModeDiscriminants::ProxyOnly)
| (Some(_), DirectOrProxyModeDiscriminants::ProxyThenDirect) => {
| (Some(_), DirectOrProxyModeDiscriminants::ProxyThenDirect)
| (Some(_), DirectOrProxyModeDiscriminants::DirectThenProxy) => {
log::info!("successfully connected {kind} chat")
}
(None, DirectOrProxyModeDiscriminants::ProxyThenDirect) => log::warn!(

View File

@ -81,7 +81,6 @@ macro_rules! define_keys {
}
impl RemoteConfigKey {
#[doc = concat!("ts: `export const NetRemoteConfigKeys = [", $("'", $key, "', "),* ,"] as const;`")]
pub const KEYS: &[&str] = &[$($key),*];
#[cfg(test)]
const IDENTITIER_KEY_PAIRS: &[(&str, &str)] = &[
@ -119,6 +118,7 @@ pub enum RemoteConfigKey {
MessagesAnonymousSendSingleRecipientMessage => "grpc.MessagesAnonymousSendSingleRecipientMessage",
AttachmentsGetUploadForm => "grpc.AttachmentsGetUploadForm",
MessagesSendMessage => "grpc.MessagesSendMessage",
BackupsAnonymousGetUploadForm => "grpc.BackupsAnonymousGetUploadForm",
}
}
@ -299,6 +299,7 @@ mod tests {
let all_known_grpc_keys: HashSet<&str> = std::iter::empty()
.chain(services::AccountsAnonymous::iter().map(|x| x.into()))
.chain(services::Attachments::iter().map(|x| x.into()))
.chain(services::BackupsAnonymous::iter().map(|x| x.into()))
.chain(services::KeysAnonymous::iter().map(|x| x.into()))
.chain(services::MessagesAnonymous::iter().map(|x| x.into()))
.chain(services::Messages::iter().map(|x| x.into()))

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,9 @@ impl<T, const LEN: usize> Array<T> for [T; LEN] {
pub trait FixedLengthBincodeSerializable: 'static {
/// Should be an actual byte array type, like `[u8; 7]`.
type Array: Array<u8> + for<'a> TryFrom<&'a [u8], Error = std::array::TryFromSliceError>;
#[cfg(feature = "metadata")]
fn name() -> String;
}
/// A wrapper type that indicates that `T` should be serialized across the bridges.

View File

@ -32,11 +32,13 @@ pub fn validate_serialization<'a, T: Deserialize<'a> + PartialDefault>(
macro_rules! bridge_as_fixed_length_serializable {
($typ:ident) => {
::paste::paste! {
// Declare a marker type for TypeScript, the same as bridge_as_handle.
// (This is harmless for the other bridges.)
#[doc = "ts: `interface " $typ " { readonly __type: unique symbol; }`"]
impl FixedLengthBincodeSerializable for $typ {
type Array = [u8; [<$typ:snake:upper _LEN>]];
#[cfg(feature = "metadata")]
fn name() -> String {
let name = stringify!($typ);
name.rsplit_once("::").map(|(_, x)| x).unwrap_or(name).into()
}
}
}
};

View File

@ -5,4 +5,4 @@
// The value of this constant is updated by the script
// and should not be manually modified
pub const VERSION: &str = "0.93.2";
pub const VERSION: &str = "0.94.1";

View File

@ -312,6 +312,20 @@ pub struct MonitoringData {
pub owned: bool,
/// Search key
pub search_key: Vec<u8>,
/// Greatest counter observed in any proof step received for this search
/// key.
///
/// It includes counter values only present in frontier nodes that
/// are _not_ used in the monitoring path (only covers the ancestor nodes)
/// Tracked separately from `ptrs` to keep the monitoring algorithm
/// unmodified and following the spec precisely.
///
/// Max observed version is only used for the early version change
/// detection in `libsignal_net_chat::api::keytrans::monitor_and_search`.
/// Without it the version change will only be detected by monitor after
/// the tree root has moved to a node that includes the update, which
/// requires the log to grow by a lot.
pub max_observed_version: u32,
}
impl Debug for MonitoringData {
@ -322,6 +336,7 @@ impl Debug for MonitoringData {
ptrs,
owned,
search_key,
max_observed_version,
} = self;
let redact_bytes = |bytes: &[u8]| {
@ -339,6 +354,7 @@ impl Debug for MonitoringData {
.field("ptrs", &ptrs)
.field("owned", &owned)
.field("search_key", &redact_bytes(search_key))
.field("max_observed_version", &max_observed_version)
.finish()
}
}
@ -367,22 +383,33 @@ impl MonitoringData {
/// The greatest known version of the search key.
pub fn greatest_version(&self) -> u32 {
self.ptrs
*self
.ptrs
.values()
.chain([&self.max_observed_version])
.max()
.copied()
.expect("at least one version must be present")
}
}
impl MonitoringData {
fn into_stored(self, search_key: Vec<u8>) -> StoredMonitoringData {
let Self {
index,
pos,
ptrs,
owned,
// Prefer the search key provided as argument.
search_key: _,
max_observed_version,
} = self;
StoredMonitoringData {
index: self.index.into(),
pos: self.pos,
ptrs: self.ptrs,
owned: self.owned,
index: index.into(),
pos,
ptrs,
owned,
search_key,
max_observed_version,
}
}
}
@ -395,6 +422,7 @@ impl From<StoredMonitoringData> for MonitoringData {
ptrs,
owned,
search_key,
max_observed_version,
} = value;
Self {
index: index.try_into().expect("must be the right size"),
@ -402,6 +430,7 @@ impl From<StoredMonitoringData> for MonitoringData {
ptrs,
owned,
search_key,
max_observed_version,
}
}
}

View File

@ -21,6 +21,7 @@ message StoredMonitoringData {
map<uint64, uint32> ptrs = 3;
bool owned = 4;
bytes search_key = 5;
uint32 max_observed_version = 6;
}
message StoredAccountData {

View File

@ -807,6 +807,7 @@ impl MonitoringDataWrapper {
ptrs: HashMap::from([(ver_pos, version)]),
owned,
search_key: vec![],
max_observed_version: version,
});
}
}
@ -860,6 +861,10 @@ impl MonitoringDataWrapper {
}
}
if version > data.max_observed_version {
data.max_observed_version = version;
}
if !data.owned && owned {
data.owned = true;
}
@ -898,6 +903,15 @@ impl MonitoringDataWrapper {
data.ptrs = ptrs;
// Keep the max_observed_version up to date and tracking the maximum
// version across _all_ nodes, not just the ancestor ones used for
// `ptrs`.
if let Some(max_version) = tree_mapping.versions().flatten().max()
&& max_version > data.max_observed_version
{
data.max_observed_version = max_version;
}
Ok(())
}
@ -964,6 +978,14 @@ impl VersionExtractor<'_> {
.map(|step| Ok(get_proto_field(&step.prefix, "prefix")?.counter))
.transpose()
}
pub fn versions(&self) -> impl Iterator<Item = Option<u32>> {
self.0.values().map(|step| {
get_proto_field(&step.prefix, "prefix")
.ok()
.map(|x| x.counter)
})
}
}
fn into_sorted_pairs<K: Ord + Copy, V>(map: HashMap<K, V>) -> (Vec<K>, Vec<V>) {
@ -1125,6 +1147,7 @@ mod test {
ptrs: HashMap::from_iter([(16777215, 2)]),
owned: true,
search_key: vec![],
max_observed_version: 2,
}));
// These values were obtained by running the integration test in
// rust/net/chat/src/api/keytrans.rs and extracting positions and versions
@ -1205,6 +1228,7 @@ mod test {
ptrs: HashMap::from([(10, 1)]),
owned: true,
search_key: vec![],
max_observed_version: 1,
}));
let steps = proof_steps([(11, 1), (15, 2)]);
@ -1223,6 +1247,7 @@ mod test {
ptrs: HashMap::from([(10, 1)]),
owned: true,
search_key: vec![],
max_observed_version: 1,
}));
// later position contains a smaller version
let steps = proof_steps([(11, 0)]);
@ -1239,6 +1264,7 @@ mod test {
ptrs: HashMap::from([(10, 1)]),
owned: true,
search_key: vec![],
max_observed_version: 1,
}));
let steps = HashMap::from_iter([
@ -1259,6 +1285,7 @@ mod test {
ptrs: HashMap::from([(10, 1), (11, 2)]),
owned: true,
search_key: vec![],
max_observed_version: 2,
}));
let steps = proof_steps([(11, 3)]);
let result = wrapper.update(16, &steps);
@ -1330,4 +1357,48 @@ mod test {
let result = extractor.get(1);
assert_matches!(result, Err(Error::RequiredFieldMissing(s)) => assert!(s.contains("prefix")));
}
// A counter increment that's only visible at a frontier proof (e.g. a
// recent tombstone for a contact's old E.164) must still be reflected in
// `MonitoringData::greatest_version`, so that the version-change detector
// in `monitor_then_search` triggers a follow-up search.
//
// For a tree of size 20 with `first_pos = 0`:
// ```text
// root(0, 20) = 15
// monitoring_path(10, 0, 20) = [11, 15] (ancestors > 10)
// full_monitoring_path = [11, 15, 19] (adds frontier node 19)
// ```
// A tombstone at log position 17 is not yet in the prefix tree at sizes
// 12 or 16, so the ancestor proofs at positions 11 and 15 report the
// stored counter (0). It _is_ in the prefix tree at size 20, so the
// frontier proof at position 19 reports the updated counter (1). The
// ancestor-only walk in `find_updated_mapping` never visits the frontier
// node, so `ptrs` does not change. The greatest known version of
// the search key, however, should reflect the higher counter so that
// version change detection still works.
#[test]
fn frontier_only_version_increment_is_detected() {
let mut wrapper = MonitoringDataWrapper::new(Some(MonitoringData {
index: [0; 32],
pos: 0,
ptrs: HashMap::from([(10, 0)]),
owned: false,
search_key: vec![],
max_observed_version: 0,
}));
let steps = proof_steps([
// Ancestor nodes
(11, 0),
(15, 0),
// Frontier node with updated version counter
(19, 1),
]);
wrapper.update(20, &steps).expect("valid test data");
let data = wrapper.inner.expect("valid test data");
assert_eq!(1, data.greatest_version(),);
}
}

View File

@ -35,11 +35,15 @@ derive-where = { workspace = true }
derive_more = { workspace = true, features = ["debug", "from", "try_from"] }
displaydoc = { workspace = true }
either = { workspace = true }
futures = { workspace = true }
futures-util = { workspace = true }
hex = { workspace = true }
hkdf = { workspace = true }
hmac = { workspace = true }
http = { workspace = true }
http-body-util = { workspace = true }
hyper = { workspace = true, features = ["server"] } # for FakeChatRemote
hyper-util = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
nonzero_ext = { workspace = true }
@ -77,9 +81,6 @@ libsignal-net = { path = ".", features = ["test-util"] }
clap = { workspace = true, features = ["derive", "env"] }
either = { workspace = true }
env_logger = { workspace = true }
futures = { workspace = true }
hyper = { workspace = true }
hyper-util = { workspace = true }
proptest = { workspace = true }
rand_chacha = { workspace = true }
test-case = { workspace = true }

View File

@ -68,6 +68,7 @@ nonzero_ext = { workspace = true }
pretty_assertions = { workspace = true }
rand = { workspace = true }
rand_chacha = { workspace = true }
scopeguard = { workspace = true }
test-case = { workspace = true }
test-log = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "test-util"] }

View File

@ -19,7 +19,7 @@ use libsignal_keytrans::{
AccountData, ChatDistinguishedResponse, ChatMonitorResponse, ChatSearchResponse,
CondensedTreeSearchResponse, FullSearchResponse, FullTreeHead, KeyTransparency, LastTreeHead,
LocalStateUpdate, MonitorContext, MonitorKey, MonitorProof, MonitorRequest, MonitorResponse,
SearchContext, SearchStateUpdate, SlimSearchRequest,
SearchContext, SearchStateUpdate, SlimSearchRequest, StoredAccountData,
};
use libsignal_net::env::KeyTransConfig;
use libsignal_protocol::PublicKey;
@ -538,6 +538,20 @@ impl UnauthenticatedChatApi for KeyTransparencyClient<'_> {
}
}
pub trait AccountDataFieldReset {
fn reset(self, field: AccountDataField) -> Self;
}
impl AccountDataFieldReset for StoredAccountData {
fn reset(mut self, field: AccountDataField) -> Self {
match field {
AccountDataField::E164 => self.e164 = None,
AccountDataField::UsernameHash => self.username_hash = None,
}
self
}
}
#[cfg(test)]
pub(crate) mod test_support {
use std::cell::Cell;
@ -758,8 +772,9 @@ pub(crate) mod test_support {
#[cfg(test)]
mod test {
use assert_matches::assert_matches;
use libsignal_keytrans::StoredMonitoringData;
use prost::Message as _;
use test_case::test_case;
use test_case::{test_case, test_matrix};
use super::test_support::{
CHAT_SEARCH_RESPONSE, CHAT_SEARCH_RESPONSE_VALID_AT, KEYTRANS_CONFIG_STAGING, test_account,
@ -859,4 +874,37 @@ mod test {
assert_eq!(skip.to_vec(), missing_fields.into_iter().collect::<Vec<_>>())
);
}
#[test_matrix([AccountDataField::E164, AccountDataField::UsernameHash])]
fn reset_account_data_field(field: AccountDataField) {
let field_data = StoredMonitoringData::default();
let data = StoredAccountData {
aci: None,
e164: Some(StoredMonitoringData {
pos: 1,
..field_data.clone()
}),
username_hash: Some(StoredMonitoringData {
pos: 2,
..field_data
}),
last_tree_head: None,
};
let updated = data.clone().reset(field);
match field {
AccountDataField::E164 => {
assert!(updated.e164.is_none());
assert_matches!(
updated.username_hash,
Some(StoredMonitoringData { pos: 2, .. })
);
}
AccountDataField::UsernameHash => {
assert_matches!(updated.e164, Some(StoredMonitoringData { pos: 1, .. }));
assert!(updated.username_hash.is_none());
}
}
}
}

View File

@ -6,12 +6,16 @@
use std::collections::BTreeSet;
/// A tag identifying an optional field in [`libsignal_keytrans::AccountData`]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, displaydoc::Display)]
#[repr(u8)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, displaydoc::Display, derive_more::TryFrom,
)]
#[try_from(repr)]
pub enum AccountDataField {
/// E.164
E164,
E164 = 0,
/// Username hash
UsernameHash,
UsernameHash = 1,
}
/// This struct adds to its type parameter a (potentially empty) list of

View File

@ -346,6 +346,25 @@ impl SearchVersions {
.flatten()
.max()
}
fn short_description(&self) -> String {
fn opt(x: &Option<impl ToString>) -> String {
x.as_ref()
.map(ToString::to_string)
.unwrap_or("_".to_string())
}
let Self {
aci,
e164,
username_hash,
} = self;
format!(
"[aci: {}, e164: {}, username_hash: {}]",
opt(aci),
opt(e164),
opt(username_hash)
)
}
}
impl<'a> Action<'a> {
@ -693,6 +712,8 @@ async fn monitor_then_search<'a>(
distinguished_tree_head: &LastTreeHead,
mode: CheckMode,
) -> Result<MaybePartial<AccountData>, RequestError<Error>> {
let stored_versions = SearchVersions::from_account_data(&stored_account_data);
log::info!("Stored versions: {}", stored_versions.short_description());
let monitor_account_data = {
let Parameters {
aci,
@ -711,8 +732,8 @@ async fn monitor_then_search<'a>(
// Call to `monitor` guarantees that the optionality of E.164 and username hash data
// will match between `stored_account_data` and `monitor_account_data`. Meaning, they will
// either both be Some() or both None.
let stored_versions = SearchVersions::from_account_data(&stored_account_data);
let updated_versions = SearchVersions::from_account_data(&monitor_account_data);
log::info!("Updated versions: {}", stored_versions.short_description());
let version_delta = updated_versions
.try_subtract(&stored_versions)
.map_err(|_| {
@ -765,8 +786,8 @@ mod test {
use test_case::{test_case, test_matrix};
use super::{
Action, Parameters, PostMonitorAction, TreeHeadWithTimestamp, VersionChanged, check,
is_too_old, merge_account_data, modal_search, monitor_then_search,
Action, Parameters, PostMonitorAction, SearchVersions, TreeHeadWithTimestamp,
VersionChanged, check, is_too_old, merge_account_data, modal_search, monitor_then_search,
select_baseline_tree_head,
};
use crate::api::RequestError;
@ -2036,4 +2057,17 @@ mod test {
ControlFlow::Break(expected),
);
}
#[test]
fn search_versions_as_short_string() {
assert_eq!(
"[aci: 42, e164: _, username_hash: 73]",
SearchVersions {
aci: Some(42),
e164: None,
username_hash: Some(73),
}
.short_description()
)
}
}

View File

@ -78,6 +78,68 @@ impl<T: GrpcService + Clone + Sync> GrpcServiceProvider for T {
}
}
/// A tonic encoder and decoder that passes byte buffers through unchanged, letting tonic
/// add the gRPC framing and nothing else.
struct PassthroughCodec;
impl tonic::codec::Codec for PassthroughCodec {
type Encode = Vec<u8>;
type Decode = Vec<u8>;
type Encoder = Self;
type Decoder = Self;
fn encoder(&mut self) -> Self::Encoder {
PassthroughCodec
}
fn decoder(&mut self) -> Self::Decoder {
PassthroughCodec
}
}
impl tonic::codec::Encoder for PassthroughCodec {
type Item = Vec<u8>;
type Error = tonic::Status;
fn encode(
&mut self,
item: Self::Item,
dst: &mut tonic::codec::EncodeBuf<'_>,
) -> Result<(), Self::Error> {
use bytes::BufMut;
dst.put(&item[..]);
Ok(())
}
}
impl tonic::codec::Decoder for PassthroughCodec {
type Item = Vec<u8>;
type Error = tonic::Status;
fn decode(
&mut self,
src: &mut tonic::codec::DecodeBuf<'_>,
) -> Result<Option<Self::Item>, Self::Error> {
use bytes::Buf;
Ok(Some(src.copy_to_bytes(src.remaining()).into()))
}
}
pub fn raw_grpc(
log_tag: &'static str,
service_provider: impl GrpcServiceProvider,
service_name: &str,
method: &str,
payload: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, RequestError<Infallible>>> {
let mut client = tonic::client::Grpc::new(service_provider.service());
let path = http::uri::PathAndQuery::from_maybe_shared(format!("/{service_name}/{method}"))
.expect("valid URI path");
log_and_send(log_tag, method, || async move {
let response = client
.unary(tonic::Request::new(payload), path, PassthroughCodec)
.await?;
Ok(response.into_inner())
})
}
async fn log_and_send<F, R, E>(
log_tag: &'static str,
log_safe_description: &str,
@ -314,7 +376,7 @@ fn request_error_from_server_side_error_info<E>(
}
"RESOURCE_EXHAUSTED" | "UNAVAILABLE" => {
// UNAVAILABLE is unlikely to have RetryInfo, but it doesn't really hurt to check.
if let Some(mut retry_delay) =
if let Some(retry_delay) =
matching_details::<google::rpc::RetryInfo>(&grpc_status.details)
.at_most_one()
.unwrap_or_else(|mut e| {
@ -325,10 +387,23 @@ fn request_error_from_server_side_error_info<E>(
})
.and_then(|info| info.retry_delay)
{
retry_delay.normalize();
// TODO: Use i32::div_ceil when that's stabilized.
// https://github.com/rust-lang/rust/issues/88581
fn nanos_to_secs_ceil(dividend: i32) -> i32 {
const DIVISOR: i32 = 1_000_000_000;
// Normal Div rounds towards 0.
let result = dividend / DIVISOR;
if dividend > 0 && dividend % DIVISOR != 0 {
result + 1
} else {
result
}
}
// Round up so that we're guaranteed to wait *at least* this long.
let retry_after_seconds =
retry_delay.seconds + i64::from(retry_delay.nanos.clamp(0, 1));
let retry_after_seconds = retry_delay
.seconds
.saturating_add(nanos_to_secs_ceil(retry_delay.nanos).into());
return RequestError::RetryLater(RetryLater {
retry_after_seconds: u32::try_from(
retry_after_seconds.clamp(0, u32::MAX.into()),
@ -424,10 +499,14 @@ pub(crate) mod testutil {
use crate::api::testutil::TEST_SELF_ACI;
use crate::ws::WsConnection;
pub(crate) fn req(uri: &str, body: impl prost::Message + 'static) -> http::Request<Vec<u8>> {
let body = tonic::codec::EncodeBody::new_client(
tonic_prost::ProstEncoder::new(Default::default()),
futures_util::stream::iter([Ok(body)]),
pub(crate) fn encode_for_grpc<C: tonic::codec::Encoder<Error = Status>>(
encoder: C,
item: C::Item,
) -> Vec<u8> {
// The difference between client and server only seems to matter when using compression.
tonic::codec::EncodeBody::new_client(
encoder,
futures_util::stream::iter([Ok(item)]),
None,
None,
)
@ -436,8 +515,11 @@ pub(crate) mod testutil {
.expect("non-blocking encoding")
.expect("can read entire message")
.to_bytes()
.into();
.into()
}
pub(crate) fn req(uri: &str, body: impl prost::Message + 'static) -> http::Request<Vec<u8>> {
let body = encode_for_grpc(tonic_prost::ProstEncoder::new(Default::default()), body);
req_typed(uri, body)
}
@ -475,6 +557,10 @@ pub(crate) mod testutil {
Status::new(code, "").into_http()
}
/// Validates that the [`WsConnection`] implementation of an API defers to the gRPC
/// implementation when the `message` override is provided.
///
/// Then defers to the inner validator for further checking and producing a response.
pub(crate) struct GrpcOverrideRequestValidator<V> {
pub(crate) validator: V,
pub(crate) message: &'static str,
@ -506,6 +592,13 @@ pub(crate) mod testutil {
}
}
/// Validates that a gRPC request matches in all parts of the underlying HTTP request, checking
/// the body byte-for-byte.
///
/// Prefer a [`GrpcOverrideRequestValidator`] containing a `RequestValidator` if the request has
/// a corresponding config to switch between WS and gRPC implementations. Replace the
/// `RequestValidator` with `TypedRequestValidator` if comparing the bodies using protobuf
/// semantics (rather than bytewise) is important---it usually isn't.
pub(crate) struct RequestValidator {
pub expected: http::Request<Vec<u8>>,
pub response: http::Response<Vec<u8>>,
@ -554,7 +647,10 @@ pub(crate) mod testutil {
/// Like `RequestValidator`, but compares the decoded protobuf of the incoming request instead
/// of the serialized bytes.
///
/// Prefer `RequestValidator`
/// Prefer `RequestValidator` if the protobuf does not contain any `map` fields, because it also
/// checks that there are no extraneous fields in the body. (While protobuf permits fields to
/// appear in any order, our prost implementation is consistent within a build, if not
/// necessarily across versions. `map` is only a problem because it uses Rust's HashMap.)
pub(crate) struct TypedRequestValidator<T> {
pub expected: http::Request<T>,
pub response: http::Response<Vec<u8>>,
@ -598,6 +694,29 @@ pub(crate) mod testutil {
}
}
/// Use to check that no gRPC calls happen at all (e.g. for a `should_panic` test, but don't
/// forget to check the panic message in that case!).
pub(crate) struct UnreachableValidator;
impl tower_service::Service<http::Request<tonic::body::Body>> for &'_ UnreachableValidator {
type Response = http::Response<http_body_util::Full<bytes::Bytes>>;
type Error = hyper::Error;
type Future = std::future::Pending<Result<Self::Response, Self::Error>>;
fn poll_ready(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
unreachable!("should not attempt to send");
}
fn call(&mut self, _req: http::Request<tonic::body::Body>) -> Self::Future {
unreachable!("should not attempt to send");
}
}
/// A protoscope-like helper type for decoding arbitrary protobuf messages.
///
/// Always succeeds as long as the input is not malformed. Only intended for debugging.
@ -887,13 +1006,13 @@ mod test {
fn test_retry_later(reason: &str) {
let info = vec![
google::rpc::RetryInfo {
retry_delay: Some(prost_types::Duration {
retry_delay: Some(libsignal_net_grpc::Duration {
seconds: 10,
nanos: 2,
}),
},
google::rpc::RetryInfo {
retry_delay: Some(prost_types::Duration {
retry_delay: Some(libsignal_net_grpc::Duration {
seconds: 20,
nanos: 5,
}),

View File

@ -151,12 +151,13 @@ mod test {
use std::fmt::Debug;
use futures_util::FutureExt as _;
use libsignal_net_grpc::proto::chat::services;
use test_case::test_case;
use super::*;
use crate::api::backups::UnauthenticatedChatApi;
use crate::api::testutil::fixed_seed_test_rng;
use crate::grpc::testutil::{RequestValidator, err, ok, req};
use crate::grpc::testutil::{GrpcOverrideRequestValidator, RequestValidator, err, ok, req};
/// A variation of `==` that ignores header order, since the gRPC encoding of this type uses a
/// protobuf map for the headers, which is not guaranteed to preserve order.
@ -212,19 +213,22 @@ mod test {
fn test_get_upload_form(
response: http::Response<Vec<u8>>,
) -> Result<UploadForm, RequestError<GetUploadFormFailure>> {
let validator = RequestValidator {
expected: req(
"/org.signal.chat.backup.BackupsAnonymous/GetUploadForm",
GetUploadFormRequest {
signed_presentation: Some(SignedPresentation {
presentation: BackupAuth::EXPECTED_PRESENTATION.to_vec(),
presentation_signature: BackupAuth::EXPECTED_SIGNATURE.to_vec(),
}),
upload_type: Some(UploadType::Messages(MessagesUploadType {})),
upload_length: 12345,
},
),
response,
let validator = GrpcOverrideRequestValidator {
message: services::BackupsAnonymous::GetUploadForm.into(),
validator: RequestValidator {
expected: req(
"/org.signal.chat.backup.BackupsAnonymous/GetUploadForm",
GetUploadFormRequest {
signed_presentation: Some(SignedPresentation {
presentation: BackupAuth::EXPECTED_PRESENTATION.to_vec(),
presentation_signature: BackupAuth::EXPECTED_SIGNATURE.to_vec(),
}),
upload_type: Some(UploadType::Messages(MessagesUploadType {})),
upload_length: 12345,
},
),
response,
},
};
Unauth(&validator)
@ -273,19 +277,22 @@ mod test {
fn test_get_media_upload_form(
response: http::Response<Vec<u8>>,
) -> Result<UploadForm, RequestError<GetUploadFormFailure>> {
let validator = RequestValidator {
expected: req(
"/org.signal.chat.backup.BackupsAnonymous/GetUploadForm",
GetUploadFormRequest {
signed_presentation: Some(SignedPresentation {
presentation: BackupAuth::EXPECTED_PRESENTATION.to_vec(),
presentation_signature: BackupAuth::EXPECTED_SIGNATURE.to_vec(),
}),
upload_length: 12345,
upload_type: Some(UploadType::Media(MediaUploadType {})),
},
),
response,
let validator = GrpcOverrideRequestValidator {
message: services::BackupsAnonymous::GetUploadForm.into(),
validator: RequestValidator {
expected: req(
"/org.signal.chat.backup.BackupsAnonymous/GetUploadForm",
GetUploadFormRequest {
signed_presentation: Some(SignedPresentation {
presentation: BackupAuth::EXPECTED_PRESENTATION.to_vec(),
presentation_signature: BackupAuth::EXPECTED_SIGNATURE.to_vec(),
}),
upload_length: 12345,
upload_type: Some(UploadType::Media(MediaUploadType {})),
},
),
response,
},
};
Unauth(&validator)

View File

@ -599,8 +599,8 @@ mod test {
use crate::api::testutil::{SERIALIZED_GROUP_SEND_TOKEN, structurally_valid_group_send_token};
use crate::api::{ChallengeOption, RateLimitChallenge};
use crate::grpc::testutil::{
GrpcOverrideRequestValidator, RequestValidator, TypedRequestValidator, err, ok, req,
req_typed,
GrpcOverrideRequestValidator, RequestValidator, TypedRequestValidator,
UnreachableValidator, err, ok, req, req_typed,
};
const ACI_UUID: Uuid = uuid!("9d0652a3-dcc3-4d11-975f-74d61598733f");
@ -778,14 +778,7 @@ mod test {
#[test]
#[should_panic(expected = "online-only")]
fn ephemeral_story_is_not_allowed() {
let validator = RequestValidator {
expected: req(
"/org.signal.chat.messages.MessagesAnonymous/SendMultiRecipientStory",
SendMultiRecipientStoryRequest::default(),
),
response: err(tonic::Code::FailedPrecondition),
};
let validator = UnreachableValidator;
_ = Unauth(&validator)
.send_multi_recipient_message(
vec![1, 2, 3].into(),
@ -1149,14 +1142,7 @@ mod test {
#[test]
#[should_panic(expected = "online-only")]
fn ephemeral_story_is_not_allowed_single_recipient() {
let validator = RequestValidator {
expected: req(
"/org.signal.chat.messages.MessagesAnonymous/SendStory",
SendMultiRecipientStoryRequest::default(),
),
response: err(tonic::Code::FailedPrecondition),
};
let validator = UnreachableValidator;
_ = Unauth(&validator)
.send_message(
Pni::from(PNI_UUID).into(),

View File

@ -49,26 +49,23 @@ impl<T: GrpcServiceProvider> crate::api::profiles::UnauthenticatedAccountExisten
}
#[cfg(test)]
mod test_account_exists {
mod test {
use assert_matches::assert_matches;
use futures_util::FutureExt;
use libsignal_core::{Aci, Pni};
use libsignal_net_grpc::proto::chat::services;
use test_case::test_case;
use test_case::test_matrix;
use uuid::{Uuid, uuid};
use super::*;
use crate::api::profiles::UnauthenticatedAccountExistenceApi;
use crate::grpc::testutil::{self, GrpcOverrideRequestValidator, RequestValidator, req};
use crate::grpc::testutil::{GrpcOverrideRequestValidator, RequestValidator, err, ok, req};
const ACI_UUID: Uuid = uuid!("9d0652a3-dcc3-4d11-975f-74d61598733f");
const PNI_UUID: Uuid = uuid!("796abedb-ca4e-4f18-8803-1fde5b921f9f");
#[test_case(Aci::from(ACI_UUID).into(), true)]
#[test_case(Pni::from(PNI_UUID).into(), true)]
#[test_case(Aci::from(ACI_UUID).into(), false)]
#[test_case(Pni::from(PNI_UUID).into(), false)]
#[tokio::test]
async fn test_it(service_id: ServiceId, found: bool) {
#[test_matrix([Aci::from(ACI_UUID).into(), Pni::from(PNI_UUID).into()], [false, true])]
fn test_account_exists(service_id: ServiceId, found: bool) {
let validator = GrpcOverrideRequestValidator {
message: services::AccountsAnonymous::CheckAccountExistence.into(),
validator: RequestValidator {
@ -78,7 +75,7 @@ mod test_account_exists {
service_identifier: Some(service_id.into()),
},
),
response: testutil::ok(CheckAccountExistenceResponse {
response: ok(CheckAccountExistenceResponse {
account_exists: found,
}),
},
@ -91,8 +88,8 @@ mod test_account_exists {
assert_eq!(result, found);
}
#[tokio::test]
async fn test_invalid() {
#[test]
fn test_account_exists_invalid() {
let validator = GrpcOverrideRequestValidator {
message: services::AccountsAnonymous::CheckAccountExistence.into(),
validator: RequestValidator {
@ -102,7 +99,7 @@ mod test_account_exists {
service_identifier: Some(Aci::from(ACI_UUID).into()),
},
),
response: testutil::err(tonic::Code::DeadlineExceeded),
response: err(tonic::Code::DeadlineExceeded),
},
};
let result = Unauth(&validator)
@ -110,6 +107,6 @@ mod test_account_exists {
.now_or_never()
.expect("sync")
.expect_err("should fail");
assert!(matches!(result, RequestError::Timeout));
assert_matches!(result, RequestError::Timeout);
}
}

View File

@ -123,12 +123,13 @@ mod test {
use data_encoding_macro::base64url_nopad;
use futures_util::FutureExt as _;
use libsignal_net_grpc::proto::chat::common::{IdentityType, ServiceIdentifier};
use libsignal_net_grpc::proto::chat::services;
use test_case::test_case;
use uuid::{Uuid, uuid};
use super::*;
use crate::api::usernames::UnauthenticatedChatApi;
use crate::grpc::testutil::{RequestValidator, err, ok, req};
use crate::grpc::testutil::{GrpcOverrideRequestValidator, RequestValidator, err, ok, req};
const ACI_UUID: Uuid = uuid!("9d0652a3-dcc3-4d11-975f-74d61598733f");
@ -169,14 +170,17 @@ mod test {
// Not realistic, but not likely to show up by accident.
let hash = &[0x00, 0xff, 0xff, 0xff];
let validator = RequestValidator {
expected: req(
"/org.signal.chat.account.AccountsAnonymous/LookupUsernameHash",
LookupUsernameHashRequest {
username_hash: hash.to_vec(),
},
),
response,
let validator = GrpcOverrideRequestValidator {
message: services::AccountsAnonymous::LookupUsernameHash.into(),
validator: RequestValidator {
expected: req(
"/org.signal.chat.account.AccountsAnonymous/LookupUsernameHash",
LookupUsernameHashRequest {
username_hash: hash.to_vec(),
},
),
response,
},
};
Unauth(&validator)
@ -205,14 +209,17 @@ mod test {
fn test_link_lookup(
response: http::Response<Vec<u8>>,
) -> Result<Option<String>, RequestError<usernames::UsernameLinkError>> {
let validator = RequestValidator {
expected: req(
"/org.signal.chat.account.AccountsAnonymous/LookupUsernameLink",
LookupUsernameLinkRequest {
username_link_handle: uuid::Uuid::nil().as_bytes().to_vec(),
},
),
response,
let validator = GrpcOverrideRequestValidator {
message: services::AccountsAnonymous::LookupUsernameLink.into(),
validator: RequestValidator {
expected: req(
"/org.signal.chat.account.AccountsAnonymous/LookupUsernameLink",
LookupUsernameLinkRequest {
username_link_handle: uuid::Uuid::nil().as_bytes().to_vec(),
},
),
response,
},
};
Unauth(&validator)

View File

@ -356,6 +356,7 @@ mod testutil {
tokio::runtime::Handle::current(),
DropOnDisconnect::new(on_disconnect).into_listener(),
[],
[],
);
async {
let _ignore_failure = self.remote.send(fake_remote);

View File

@ -554,6 +554,7 @@ mod test {
tokio::runtime::Handle::current(),
DropOnDisconnect::new(on_disconnect).into_listener(),
[],
[],
);
fake_chat_tx.send(fake_remote).unwrap();
Ok(Unauth(fake_chat))
@ -634,7 +635,7 @@ mod test {
remote: fake_chat_remote_tx,
};
let (request_sender, _join_handle) = spawn_connected_chat(&fake_connect)
let (request_sender, join_handle) = spawn_connected_chat(&fake_connect)
.await
.expect("can connect");
let fake_chat_remote = fake_chat_remote_rx.recv().await.unwrap();
@ -686,6 +687,7 @@ mod test {
let _response = first_send_fut.await;
// The task should reach its inactivity timeout and disconnect.
join_handle.await.expect("no panic");
assert_matches!(fake_chat_remote.receive_request().await, Ok(None));
}
}

View File

@ -49,6 +49,12 @@ impl<T: WsConnection> crate::api::backups::UnauthenticatedChatApi<OverWs> for Un
upload_size: u64,
rng: &mut (dyn rand::CryptoRng + Send),
) -> Result<UploadForm, RequestError<GetUploadFormFailure>> {
if let Some(grpc) =
self.grpc_service_to_use_instead(services::BackupsAnonymous::GetUploadForm.into())
{
return Unauth(grpc).get_upload_form(auth, upload_size, rng).await;
}
let auth = auth.present(rng)?;
let response = self

View File

@ -5,17 +5,32 @@ edition = "2021"
authors = ["Signal Messenger LLC"]
license = "AGPL-3.0-only"
[features]
json = [
"dep:pbjson",
"dep:pbjson-build",
"dep:pbjson-types",
"dep:serde",
"dep:serde_json",
]
[dependencies]
libsignal-core = { workspace = true }
const-str = { workspace = true }
derive-where = { workspace = true }
pbjson = { workspace = true, optional = true }
pbjson-types = { workspace = true, optional = true }
prost = { workspace = true }
prost-types = { workspace = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
strum = { workspace = true, features = ["derive"] }
tonic = { workspace = true, default-features = false, features = ["codegen"] }
tonic-prost = { workspace = true }
[build-dependencies]
pbjson-build = { workspace = true, optional = true }
tonic-prost-build = { workspace = true }
[lib]

View File

@ -35,9 +35,32 @@ fn main() {
service_method_file.push("service_methods.rs");
std::fs::write(service_method_file, service_method_contents).expect("can write to OUT_DIR");
tonic_prost_build::configure()
#[cfg(feature = "json")]
{
let mut json_build = pbjson_build::Builder::new();
for fd in &fds.file {
json_build.register_file_descriptor(fd.clone());
}
json_build
.build(&[".org.signal.chat"])
.expect("can compile with pbjson");
}
let mut tonic_build = tonic_prost_build::configure()
.build_server(false)
.build_transport(false)
.build_transport(false);
if cfg!(feature = "json") {
tonic_build = tonic_build
.compile_well_known_types(true)
.extern_path(".google.protobuf", "::pbjson_types")
// Note that this diverges from proper protobuf JSON in the interest of simplicity and
// prost_types compatibility. (Empty would normally be encoded as `{}`, but `()` is
// encoded as `null`.)
.extern_path(".google.protobuf.Empty", "()")
// These are only used for generic errors, not requests and responses.
.extern_path(".google.protobuf.Any", "::prost_types::Any");
}
tonic_build
.compile_fds_with_config(fds, config)
.expect("can generate code");
}

69
rust/net/grpc/src/json.rs Normal file
View File

@ -0,0 +1,69 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::collections::HashMap;
use std::sync::LazyLock;
pub fn expect_binproto_to_json<T: prost::Message + Default + serde::Serialize>(
input: &[u8],
) -> String {
serde_json::to_string(&T::decode(input).expect("valid input")).expect("can encode as JSON")
}
pub fn expect_json_to_binproto<T: prost::Message + serde::de::DeserializeOwned>(
input: &str,
) -> Vec<u8> {
serde_json::from_str::<T>(input)
.expect("valid JSON")
.encode_to_vec()
}
pub fn expect_binproto_to_json_by_name(message_name: &str, input: &[u8]) -> String {
type BinprotoToJsonFn = fn(&[u8]) -> String;
// TODO: generate this
static OPS: LazyLock<HashMap<&'static str, BinprotoToJsonFn>> = LazyLock::new(|| {
HashMap::from_iter([
(
"org.signal.chat.account.LookupUsernameHashRequest",
expect_binproto_to_json::<crate::proto::chat::account::LookupUsernameHashRequest>
as _,
),
(
"org.signal.chat.account.LookupUsernameLinkRequest",
expect_binproto_to_json::<crate::proto::chat::account::LookupUsernameLinkRequest>
as _,
),
])
});
let op = OPS
.get(message_name)
.unwrap_or_else(|| unimplemented!("missing binproto_to_json for {message_name}"));
op(input)
}
pub fn expect_json_to_binproto_by_name(message_name: &str, input: &str) -> Vec<u8> {
type JsonToBinprotoFn = fn(&str) -> Vec<u8>;
// TODO: generate this
static OPS: LazyLock<HashMap<&'static str, JsonToBinprotoFn>> = LazyLock::new(|| {
HashMap::from_iter([
(
"org.signal.chat.account.LookupUsernameHashResponse",
expect_json_to_binproto::<crate::proto::chat::account::LookupUsernameHashResponse>
as _,
),
(
"org.signal.chat.account.LookupUsernameLinkResponse",
expect_json_to_binproto::<crate::proto::chat::account::LookupUsernameLinkResponse>
as _,
),
])
});
let op = OPS
.get(message_name)
.unwrap_or_else(|| unimplemented!("missing json_to_binproto for {message_name}"));
op(input)
}

View File

@ -5,28 +5,45 @@
#![warn(clippy::unwrap_used)]
#[cfg(feature = "json")]
pub mod json;
pub mod proto {
pub mod chat {
pub mod common {
tonic::include_proto!("org.signal.chat.common");
#[cfg(feature = "json")]
tonic::include_proto!("org.signal.chat.common.serde");
}
pub mod errors {
tonic::include_proto!("org.signal.chat.errors");
#[cfg(feature = "json")]
tonic::include_proto!("org.signal.chat.errors.serde");
}
pub mod account {
tonic::include_proto!("org.signal.chat.account");
#[cfg(feature = "json")]
tonic::include_proto!("org.signal.chat.account.serde");
}
pub mod attachments {
tonic::include_proto!("org.signal.chat.attachments");
#[cfg(feature = "json")]
tonic::include_proto!("org.signal.chat.attachments.serde");
}
pub mod backup {
tonic::include_proto!("org.signal.chat.backup");
#[cfg(feature = "json")]
tonic::include_proto!("org.signal.chat.backup.serde");
}
pub mod device {
tonic::include_proto!("org.signal.chat.device");
#[cfg(feature = "json")]
tonic::include_proto!("org.signal.chat.device.serde");
}
pub mod messages {
tonic::include_proto!("org.signal.chat.messages");
#[cfg(feature = "json")]
tonic::include_proto!("org.signal.chat.messages.serde");
}
// Not actually a proto, we just make sure to generate our helper file in the same place.
@ -49,6 +66,11 @@ pub mod proto {
}
}
#[cfg(not(feature = "json"))]
pub type Duration = prost_types::Duration;
#[cfg(feature = "json")]
pub type Duration = pbjson_types::Duration;
impl From<libsignal_core::ServiceId> for proto::chat::common::ServiceIdentifier {
fn from(value: libsignal_core::ServiceId) -> Self {
let kind = match value.kind() {
@ -139,3 +161,23 @@ impl prost::Name for proto::google::rpc::RetryInfo {
.to_owned()
}
}
/// Manual implementation of the gRPC framing format (Length-Prefixed-Message).
///
/// tonic normally takes care of this for us on the Rust side, but app-level tests (using e.g.
/// `FakeChatRemote`) have to deal with the raw HTTP bodies.
///
/// See <https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md>.
pub fn expect_next_grpc_message_for_testing(input: &[u8]) -> &[u8] {
const HEADER_LEN: usize = 5;
assert!(input.len() >= HEADER_LEN, "unexpected EOF");
assert_eq!(input[0], 0, "compression not supported");
let message_length =
u32::from_be_bytes(*input[1..].first_chunk().expect("already checked length"));
let message_length = usize::try_from(message_length).expect("at least 32-bit usize");
assert!(
message_length + HEADER_LEN <= input.len(),
"message length exceeds remaining input"
);
&input[HEADER_LEN..][..message_length]
}

View File

@ -209,6 +209,10 @@ impl<Transport: UsesTransport<UnresolvedTransportRoute>> DescribeForLog
},
inner: _,
}) => (target_host.as_informational_host(), *target_port),
ConnectionProxyRoute::Reflector(reflector) => (
Host::Domain(reflector.target_host.clone()),
DEFAULT_HTTPS_PORT,
),
},
};

View File

@ -14,8 +14,8 @@ use crate::certs::RootCertificates;
use crate::errors::LogSafeDisplay;
use crate::host::Host;
use crate::route::{
ReplaceFragment, RouteProvider, RouteProviderContext, SimpleRoute, TcpRoute, TlsRoute,
TlsRouteFragment, UnresolvedHost,
HttpsTlsRoute, ReplaceFragment, RouteProvider, RouteProviderContext, SimpleRoute, TcpRoute,
TlsRoute, TlsRouteFragment, UnresolvedHost, WebSocketRoute,
};
use crate::tcp_ssl::proxy::socks;
use crate::{Alpn, OverrideNagleAlgorithm};
@ -53,6 +53,12 @@ pub struct HttpProxyAuth {
pub password: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ReflectorProxyRoute<Addr> {
pub outer: WebSocketRoute<HttpsTlsRoute<TlsRoute<TcpRoute<Addr>>>>,
pub target_host: Arc<str>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, strum::EnumDiscriminants)]
#[strum_discriminants(name(ConnectionProxyKind))]
pub enum ConnectionProxyRoute<Addr> {
@ -66,6 +72,8 @@ pub enum ConnectionProxyRoute<Addr> {
},
Socks(SocksRoute<Addr>),
Https(HttpsProxyRoute<Addr>),
// Boxed because it's much larger than the other variants.
Reflector(Box<ReflectorProxyRoute<Addr>>),
}
/// Target address for proxy protocols that support remote resolution.
@ -95,6 +103,7 @@ pub enum DirectOrProxyMode {
DirectOnly,
ProxyOnly(ConnectionProxyConfig),
ProxyThenDirect(ConnectionProxyConfig),
DirectThenProxy(ConnectionProxyConfig),
}
/// [`RouteProvider`] implementation that returns [`DirectOrProxyRoute`]s.
@ -137,6 +146,9 @@ pub struct HttpProxy {
pub resolve_hostname_locally: bool,
}
#[derive(Debug)]
pub struct ReflectorProviderConfig;
#[derive(Debug, Clone, derive_more::From)]
pub enum ConnectionProxyConfig {
Tls(TlsProxy),
@ -144,6 +156,10 @@ pub enum ConnectionProxyConfig {
Tcp(TcpProxy),
Socks(SocksProxy),
Http(HttpProxy),
/// Reflector tunnel providers to try. The caller is expected to take this
/// slice from the surrounding environment/domain config so prod and staging
/// can't be mispaired.
Reflector(&'static [ReflectorProviderConfig]),
}
#[derive(Debug, thiserror::Error, displaydoc::Display)]
@ -258,11 +274,14 @@ impl ConnectionProxyConfig {
}
pub fn is_signal_transparent_proxy(&self) -> bool {
// Here, a "signal_transparent_proxy" is one we don't want to fall back
// from on connect failure. Currently that's just `Self::Tls`.
// TODO(reflector): clean up this method naming.
match self {
Self::Tls(_) => true,
#[cfg(feature = "dev-util")]
Self::Tcp(_) => true,
Self::Socks(_) | Self::Http(_) => false,
Self::Socks(_) | Self::Http(_) | Self::Reflector(_) => false,
}
}
}
@ -322,7 +341,9 @@ where
let replacer = move |r: D::Route| replacer(r).replace(DirectOrProxyRoute::Proxy);
Either::Right(Either::Left(original_routes.map(replacer)))
}
DirectOrProxyMode::ProxyThenDirect(proxy) => {
DirectOrProxyMode::ProxyThenDirect(proxy)
| DirectOrProxyMode::DirectThenProxy(proxy) => {
let direct_first = matches!(mode, DirectOrProxyMode::DirectThenProxy(_));
let original_routes = original_routes.collect_vec();
let direct_routes = original_routes
.iter()
@ -330,18 +351,22 @@ where
.map(|r| r.replace(DirectOrProxyRoute::Direct))
.collect_vec();
let replacer = proxy.as_replacer();
let replacer = move |r: D::Route| replacer(r).replace(DirectOrProxyRoute::Proxy);
Either::Right(Either::Right(
original_routes
.into_iter()
.map(replacer)
.chain(direct_routes),
))
let proxied_routes = original_routes
.into_iter()
.map(move |r: D::Route| replacer(r).replace(DirectOrProxyRoute::Proxy))
.collect_vec();
let (first, second) = if direct_first {
(direct_routes, proxied_routes)
} else {
(proxied_routes, direct_routes)
};
Either::Right(Either::Right(first.into_iter().chain(second)))
}
}
}
}
// TODO(reflector): deep nesting of `Either` can be optimized away later.
trait AsReplacer {
fn as_replacer<R: ReplaceFragment<TcpRoute<UnresolvedHost>>>(
&self,
@ -354,26 +379,32 @@ impl AsReplacer for ConnectionProxyConfig {
) -> impl Fn(R) -> R::Replacement<ConnectionProxyRoute<Host<UnresolvedHost>>> {
let replacer = match self {
ConnectionProxyConfig::Tls(tls_proxy) => {
Either::Left(Either::Left(tls_proxy.as_replacer()))
Either::Left(Either::Left(Either::Left(tls_proxy.as_replacer())))
}
#[cfg(feature = "dev-util")]
ConnectionProxyConfig::Tcp(tcp_proxy) => {
Either::Right(Either::Left(tcp_proxy.as_replacer()))
Either::Left(Either::Right(Either::Left(tcp_proxy.as_replacer())))
}
ConnectionProxyConfig::Socks(socks_proxy) => {
let replacer = socks_proxy.as_replacer();
#[cfg(feature = "dev-util")]
let replacer = Either::Right(replacer);
Either::Right(replacer)
Either::Left(Either::Right(replacer))
}
ConnectionProxyConfig::Http(http_proxy) => {
Either::Left(Either::Right(http_proxy.as_replacer()))
Either::Left(Either::Left(Either::Right(http_proxy.as_replacer())))
}
// TODO(reflector): reshape replacement API to handle reflectors expansion.
ConnectionProxyConfig::Reflector(_providers) => Either::Right(
|_route: R| -> R::Replacement<ConnectionProxyRoute<Host<UnresolvedHost>>> {
unimplemented!("reflector route expansion not yet implemented")
},
),
};
move |route| match &replacer {
Either::Left(Either::Left(f)) => f(route),
Either::Left(Either::Right(f)) => f(route),
Either::Right(f) => match f {
Either::Left(Either::Left(Either::Left(f))) => f(route),
Either::Left(Either::Left(Either::Right(f))) => f(route),
Either::Left(Either::Right(f)) => match f {
#[cfg(feature = "dev-util")]
Either::Left(f) => f(route),
#[cfg(feature = "dev-util")]
@ -381,6 +412,7 @@ impl AsReplacer for ConnectionProxyConfig {
#[cfg(not(feature = "dev-util"))]
f => f(route),
},
Either::Right(f) => f(route),
}
}
}

Some files were not shown because too many files have changed in this diff Show More