Compare commits

...

450 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
Jordan Rose
bbc16886ca
net: Stop processing websocket events on H2 GOAWAY 2026-05-05 17:34:41 -07:00
Jordan Rose
29adeae754 node: Support non-ASCII usernames and passwords in proxy URLs 2026-05-05 17:04:24 -07:00
moiseev-signal
df8cf83114
node: Bridge SVR2-related code 2026-05-05 16:24:54 -07:00
Jordan Rose
0929fa7cc7
net: Add a test for H2+WS behavior with H2 GOAWAY (graceful shutdown) 2026-05-05 10:11:40 -07:00
andrew-signal
b6ae27f259
Bump hickory-proto to 0.26.1 2026-05-04 17:31:22 -04:00
gram-signal
16a1b80a82
New SVR enclaves 2026-05-01 13:03:49 -07:00
gram-signal
191f78b8e2
CDSI - update production enclave. 2026-04-30 13:08:32 -07:00
andrew-signal
dc72d43c21
Improve dev ergonomics around acknowledgements 2026-04-30 15:07:24 -04:00
Max Moiseev
3e616bfca4 Reset for version v0.93.2 2026-04-29 12:40:08 -07:00
gram-signal
4c460615cd
Update CDSI staging to 6d9b9649 2026-04-29 11:41:59 -07:00
Jordan Rose
309a220fe3 swift: Add AuthServiceSelector.messages (mistakenly .attachments)
.attachments continues to work for now but will be removed in the
future.
2026-04-28 16:44:04 -07:00
Jordan Rose
ce7b55a3af swift: Ensure that a SliceOfBuffers borrow keeps its buffers alive 2026-04-28 16:44:04 -07:00
Jordan Rose
12228b31e7 chat: Add a debug-level log for uncategorized gRPC failures
Unfortunately these aren't necessarily log-safe, so we can't log the
message in production.
2026-04-28 16:41:07 -07:00
Max Moiseev
7c6ef18903 Reset for version v0.93.1 2026-04-28 14:40:37 -07:00
andrew-signal
fc0fb6fd5c
Fix cross-version protocol test local addresses 2026-04-28 14:16:31 -07:00
andrew-signal
12617f69b7
net: clamp max DNS ANCOUNT 2026-04-28 13:39:48 -04:00
andrew-signal
7d5eebee89
chore: bump rustls-webpki / zerocopy / quote 2026-04-27 22:38:38 -04:00
marc-signal
202df549ef
Check identity key and service id to determine a self-message 2026-04-27 15:21:22 -04:00
Jordan Rose
bbc6c37c1a jni: Generalize bridge_trait macro encompass all the traits it used to
...similar to what we did for Node, but a little more complex because
the Java InputStream implementation doesn't quite fit into
bridge_callbacks.
2026-04-27 10:25:35 -07:00
moiseev-signal
8582eb70d6
java: Properly handle empty attachments in incremental mac stream 2026-04-21 16:13:35 -07:00
Jordan Rose
3ae7241bc1
Expose AuthMessagesService.sendMessage and sendSyncMessage 2026-04-21 13:23:10 -07:00
Jordan Rose
bddcb4e15f
Expose UnauthMessagesService.sendMessage 2026-04-21 11:17:39 -07:00
Jordan Rose
14f4cee71f build_jni.sh: Continue building even if cargo fetch fails
Normally we `cargo fetch` as part of the script so that
`rust_remap_path_options` from build_helpers.sh/.py behaves correctly.
But if we're offline, that will fail, and it's better to go on with
whatever we have instead.
2026-04-21 08:42:01 -07:00
Jordan Rose
7962de2f9a bridge_callbacks: Accept a custom name for the Node interface 2026-04-21 07:54:31 -07:00
Jordan Rose
67a8a50c7a bridge_callbacks: Accept a custom basename for the C bridge types
cbindgen doesn't provide a way to have the Rust and C types have
different names purely via inline attribute, so it affects both...but
that actually simplifies some of our current handling. No more
"sometimes FfiBridge, sometimes just Ffi".
2026-04-21 07:54:31 -07:00
moiseev-signal
ded51f5bdb
keytrans: Add info level logs in key places 2026-04-20 14:45:18 -07:00
Jordan Rose
b41aeb1fda Update rand 0.8 as well (dev-dependency only) 2026-04-20 12:50:26 -07:00
Jordan Rose
9abd8a52d2 Improve docs for bridge_fn, bridge_io, and bridge_callbacks 2026-04-20 12:24:42 -07:00
Jordan Rose
8044869780 bridge_callbacks: Reject non-async callbacks returning values in Node
Due to Neon limitations, we can't do fully synchronous callbacks from
an arbitrary point in the call stack. A synchronous call for this
bridge is merely a synchronous *dispatch* back to the JS thread,
without waiting for completion.
2026-04-20 12:24:42 -07:00
Jordan Rose
91932c20c5 build_node_bridge: Debug builds don't have separate debug info 2026-04-20 11:51:01 -07:00
marc-signal
cac69c686a
Debug stacktrace capture performance 2026-04-20 14:18:40 -04:00
moiseev-signal
ccd54a28f9
CI: Update last outdated actions/setup-node 2026-04-20 10:45:04 -07:00
Jordan Rose
aa6769e2e2 CI: Disable SWIFT_ENABLE_EXPLICIT_MODULES for pod lib lint 2026-04-16 13:11:01 -07:00
Andrew
5967b6a9b1 Reset for version v0.92.3 2026-04-16 10:50:16 -04:00
Jordan Rose
98915c442d Update rustls-webpki 2026-04-15 15:42:15 -07:00
marc-signal
a110fe85c7
Upgrade rand crate 2026-04-13 17:35:57 -04:00
Rolfe Schmidt
5cf8b0d717
Refactor 1:1 messaging code 2026-04-13 12:17:27 -06:00
moiseev-signal
c975a83e8a
keytrans: Check that values in search response match expected ones 2026-04-10 14:33:05 -07:00
moiseev-signal
c683a45242
keytrans: Persist latest distinguished tree head in local store 2026-04-10 14:32:20 -07:00
moiseev-signal
00709fe70b
keytrans: Update distinguished tree head when needed 2026-04-10 13:53:35 -07:00
moiseev-signal
f556ac7b1f
keytrans: Update test data 2026-04-10 12:40:24 -07:00
Andrew
9178fbc04e Reset for version v0.92.2 2026-04-09 19:19:47 -04:00
marc-signal
b58bd7d5df
Bridge backup uploads to clients 2026-04-09 18:31:22 -04:00
Jordan Rose
a8083dee6e zkgroup: Generate new ExpiringProfileKeyCredentialPresentations 2026-04-09 11:40:02 -07:00
Rolfe Schmidt
3d333fa3e6
Move day alignment check for receipt credential into libsignal 2026-04-08 17:03:47 -06:00
gram-signal
5289445a4e
Enforce max # of pairs in backup metadata. 2026-04-08 13:48:27 -07:00
Jordan Rose
480021a260 Reset for version v0.92.1 2026-04-08 10:24:15 -07:00
Jordan Rose
5835078f50 java: Test verification of the V4 ProfileKeyCredentialPresentation 2026-04-07 16:09:10 -07:00
Jordan Rose
c235197dad
zkgroup: Bind C_y5 in ProfileKeyCredential proof 2026-04-07 15:08:40 -07:00
andrew-signal
1a347324e4
Fix cross-version assert_matches on stable 2026-04-07 14:53:43 -04:00
Jordan Rose
bd5e21217a java: Use Kotlin's Base64 support in tests instead of our own shim 2026-04-07 10:49:50 -07:00
gram-signal
cf9a7445c6
Force SPQR v1. 2026-04-03 11:04:42 -07:00
Jordan Rose
939690b070 chat: Replace stale comment 2026-04-03 09:18:21 -07:00
Jordan Rose
4cafee67b0 chat: Log "done" instead of "OK" when a gRPC request completes
...because it might be completing with a request-specific failure, and
"OK" looks like success.
2026-04-02 16:48:13 -07:00
andrew-signal
50a7742adf
Implement backoff on more kinds of 4xx errors 2026-04-01 21:00:07 -04:00
Max Moiseev
87ba49f645 Reset for version v0.91.1 2026-04-01 12:51:05 -07:00
marc-signal
8418be45db
Support upload size arguments in getUploadSize() 2026-04-01 15:00:16 -04:00
Rolfe Schmidt
81afdfe2fa
MAC sender ID 2026-04-01 11:22:54 -06:00
Jordan Rose
3d4e950848 node: Support H2 connections to a local server 2026-03-31 15:30:26 -07:00
Jordan Rose
f645178020 node: Replace the NPM 'uuid' package with a Rust-backed implementation 2026-03-31 11:06:13 -07:00
Jordan Rose
c077c48393 node: Use neon:🧵:LocalKey instead of an underscored JS global
...to record the logging function for the JS context. Just a little
more principled! And also more consistent if initLogger is called
twice for some reason; previously, it would update the logging
function but *not* the log level.
2026-03-31 10:21:01 -07:00
marc-signal
400a021c2b
Log a backtrace on panic 2026-03-31 12:12:40 -04:00
Jordan Rose
1bca413376 ffi: Use bridge_callbacks for Logger 2026-03-30 17:25:56 -07:00
marc-signal
a7a24ed517
Log more details on gRPC failure 2026-03-30 18:50:00 -04:00
marc-signal
c18509b108
Run swift format 2026-03-30 18:49:11 -04:00
Jordan Rose
6f48650cea net: Remove overzealous assert waiting for a websocket task to exit
If a WS request fails because a channel has closed, the task should be
*in the process* of exiting, closing its channels along the way.
However, we can't assert that it's *done* exiting. We *do* still want
to log, however, because if this helper is ever called when the task
*isn't* exiting we'd like to know why it stalled out.
2026-03-30 12:46:20 -07:00
Jordan Rose
49d53a6506 ffi: Micro-optimization for returning a String 2026-03-30 09:51:46 -07:00
Jordan Rose
8df85d3c0b
chat: Add UserBasedAuthorization::UnrestrictedUnidentifiedAccess
...for users with "allow sealed sender from everyone".
2026-03-30 09:50:48 -07:00
Jordan Rose
1feca0e32e
chat: gRPC implementation of unauth 1:1 sendMessage 2026-03-27 17:22:39 -07:00
Jordan Rose
407d7e7a27 check_code_size: Update for newer workflow and job names
...and account for `gh run view` not consistently tagging steps by
matching on a different output line of the script.
2026-03-27 16:22:12 -07:00
Rolfe Schmidt
98794de745
1:1 decryption takes local address 2026-03-27 16:45:07 -06:00
Jordan Rose
955e4c9f1d
chat: Implement gRPC 1:1 auth sends 2026-03-27 13:08:45 -07:00
marc-signal
e9ec8dd431
Add gRPC support for getUploadForm() 2026-03-27 14:39:42 -04:00
marc-signal
c7e696c536
Upgrade derive-where 2026-03-27 11:13:45 -04:00
Jordan Rose
ec675c52a6 Reset for version v0.90.1 2026-03-26 17:10:34 -07:00
marc-signal
0a58e80bbc
Add support for a retry later duration in rate limit challenge responses 2026-03-26 17:08:19 -07:00
moiseev-signal
c2db79042d
keytrans: Keep the missing field markers on version change 2026-03-26 14:59:37 -07:00
moiseev-signal
467cd795e5
keytrans: Simplify the top-level API 2026-03-26 14:30:47 -07:00
Jordan Rose
e06ff35e1a net: Pull shared_h2_connection out of the ws::Chat state mutex
Http2Client already has its own synchronization, so the mutex isn't
adding anything. This allows synchronous access to the H2 client as
well, simplifying all the callers that need to ask if it's available.
2026-03-26 13:15:07 -07:00
Jordan Rose
7ca4f738fb net: Remove explicit Drop for ws::Chat, it no longer adds anything 2026-03-26 13:15:07 -07:00
Jordan Rose
e3d9c09bec net: Make a hybrid chat's WS task cancel any shared H2 requests
...rather than relying on an explicit disconnect() call or waiting
until Drop (which may happen well after the websocket is closed).
2026-03-26 13:15:07 -07:00
andrew-signal
703dafe239
Use syntactically correct usernames to test server behavior 2026-03-26 15:44:02 -04:00
gram-signal
c3479fabe1
Update CDSI to use Jasmin backend in staging. 2026-03-26 12:00:45 -07:00
Jordan Rose
daaea0a9e9
net: Require chat-server Auth to have a valid ACI and device ID
This does not yet require the same at the app layer; the parsing and
enforcement has merely been pushed up to the bridge layer.
2026-03-25 13:10:24 -07:00
Jordan Rose
a4a1191309 Clean up signatures for auth message sends 2026-03-25 12:14:21 -07:00
Jordan Rose
bd3ee636b8 chat: Implement WS send_sync_message
(which just forwards to send_message)
2026-03-25 12:14:21 -07:00
Jordan Rose
ab73bbf262 net: For auth chat connections, remember the ACI we used in auth'ing 2026-03-25 12:14:21 -07:00
Jordan Rose
cb2990f318
chat: Add authenticated sendMessage (WS only) 2026-03-24 15:48:32 -07:00
Andrew
1f5f12f1fc Reset for version v0.89.3 2026-03-24 18:07:13 -04:00
andrew-signal
da00edb1ec
Bump tokio-util to 0.7.18, to match actual usage 2026-03-24 18:03:08 -04:00
andrew-signal
d3eaaa46b7
Remove Maven migration warning from README 2026-03-24 16:12:41 -04:00
Andrew
1283a8a00b Bump to latest Rust nightly 2026-03-24 15:52:17 -04:00
Andrew
75925e46d7 Use --locked in Node reproducible builds 2026-03-24 15:52:17 -04:00
Jordan Rose
2c0b3c9984
net: Allow cancelling outstanding Http2Client requests
...and do so on ChatConnection disconnect and Drop.
2026-03-24 12:12:14 -07:00
Jordan Rose
f0babf1537
net: Record service-level errors post-connect
A "fatal" connect error already meant "don't try any other routes this
time around". This additionally adds the idea "treat this as a
service-level failure", as a proxy for "there's something wrong at the
server end". Today, this happens when we get a confirmed 508 status
from the server upon websocket connect.
2026-03-24 11:54:35 -07:00
marc-signal
c706b7f5ce
Expose getUploadForm() to clients
Co-authored-by: Jordan Rose <jrose@signal.org>
2026-03-24 14:32:59 -04:00
Rolfe Schmidt
1c9a428c2c
Bump libcrux and spqr versions 2026-03-23 11:04:49 -06:00
andrew-signal
b9b9cf0684
Bump to latest rustls-webpki. 2026-03-23 13:02:01 -04:00
Jordan Rose
68f33f488d bridge: Collapse node::ArgTypeInfo for bridge traits back into a macro 2026-03-23 09:42:12 -07:00
Jordan Rose
db33599cad bridge: Use bridge_callbacks for InputStream in Node
Note that this isn't unified with the bridge_callbacks trait in
ffi/io.rs! The signatures are too different to be useful. However,
this is still simpler than the manual implementation that was there
before.
2026-03-23 09:42:12 -07:00
Jordan Rose
3e0c17541f net: Introduce service-level backoff as well as route-level 2026-03-20 13:39:15 -07:00
Jordan Rose
227dc23941 net: Add ServiceName to DomainConfig; thread it down to ConnectState
This will be used to track connection history on a service-wide basis,
much as the existing attempts record tracks it on a per-route basis.
2026-03-20 13:39:15 -07:00
Andrew
e667bd6c03 Reset for version v0.89.2 2026-03-19 11:06:21 -04:00
Jordan Rose
a5e7667488 Allow gRPC remote config names to have version suffixes (".123")
And bump AccountsAnonymousLookupUsernameLink,
AccountsAnonymousCheckAccountExistence, and
MessagesAnonymousSendMultiRecipientMessage to have a ".2" suffix.
2026-03-18 15:12:59 -07:00
Jordan Rose
63fd295b9d grpc: Rely on tonic's extraction of error details
...rather than trying to do it ourselves, which doesn't work because
tonic has already removed the key from the metadata.
2026-03-18 15:12:40 -07:00
Jordan Rose
84b2718fd1 grpc: Add missing dot in manually-assembled type URLs 2026-03-18 15:12:40 -07:00
moiseev-signal
8abb863cca
CI: Update most actions to Node 24 2026-03-18 14:07:01 -07:00
Jordan Rose
18f47d2cb4 chat: Add chat_request_scaffold example, for one-off testing 2026-03-18 12:10:53 -07:00
Jordan Rose
358dd5cb80
chat: Implement WS *auth* API get_upload_form 2026-03-18 10:52:42 -07:00
Cody Henthorne
c5b3a53c9c
backups: Add group terminated snapshot field and terminate group change update validation.
Co-authored-by: Max Moiseev <moiseev@signal.org>
2026-03-18 10:01:13 -07:00
Jordan Rose
8a1663387b node: Preserve more information for exceptions thrown in callbacks 2026-03-18 10:00:49 -07:00
Jordan Rose
2936450819 chat: impl Display for Redact<proto::ServiceIdentifier>
Useful for log-safe gRPC requests
2026-03-17 18:04:48 -07:00
marc-signal
1ca00428e0
Add signal-debug for Android 2026-03-17 18:51:17 -04:00
Jordan Rose
8cc2832763 Reset for version v0.89.1 2026-03-17 11:46:00 -07:00
marc-signal
a47ba487a7
Add getPreKeys() client library 2026-03-17 13:18:06 -04:00
Jamie
8bfbd12323
Update all Uint8Array/Buffer to use ArrayBuffer
Co-authored-by: Jordan Rose <jrose@signal.org>
2026-03-16 18:55:17 -07:00
Jordan Rose
863219012f Use bridge_callbacks for IdentityKeyStore in Node
And add IdentityKeyStore.getIdentityKeyPair to save a public key
derivation when implemented, similar to 55b233d43.
2026-03-16 14:42:46 -07:00
Jordan Rose
260c46ce61 CI: Check private rustdoc validity too
- Fix issues uncovered by this
- Skip documenting dependencies in CI (hopefully a bit faster)
2026-03-16 12:13:51 -07:00
marc-signal
f6c4ff2e8d
Allow kotlin.Pair getters to be called from native 2026-03-16 13:11:33 -04:00
Jordan Rose
7cfb75ea1e chat: Add JsonRequestValidator to test client->server JSON bodies 2026-03-13 17:02:16 -07:00
Jordan Rose
71183a9043
chat: Implement unauth 1:1 message sends (WS only) 2026-03-13 16:31:47 -07:00
moiseev-signal
0e51bb356b
keytrans: Preserve more account data between API calls 2026-03-13 16:03:49 -07:00
Jordan Rose
657d185fb8 bridge: Use bridge_callbacks for IdentityKeyStore in Java 2026-03-13 15:16:30 -07:00
alexanderhassler
fbe0e69889 CODING_GUIDELINES.md: fix spelling of "maintenance"
Typo fix in CODING_GUIDELINES.md. No functional change.

- Before: "our own maintainence"
- After: "our own maintenance"
2026-03-13 15:16:00 -07:00
Jordan Rose
895b079448 jni: Avoid forming &mut when destroying a bridged handle
...because this may not be the last *Rust-side* reference.
2026-03-13 15:14:49 -07:00
Max Moiseev
fe9d0e761b Reset for version v0.88.4 2026-03-13 15:02:59 -07:00
andrew-signal
c5edd3fa38
Don't abort a whole takeout JSON batch just because one line fails to render 2026-03-13 14:39:27 -07:00
Jordan Rose
be5c8df415
backup: Replace protobuf-json-mapping with pbjson
However, pbjson works with prost/prost-build instead of
protobuf/protobuf-codegen, so we now have *two* protobuf libraries in
use in libsignal-message-backup, at least when using JSON features.
When prost is able to handle unknown fields, we can get rid of
protobuf/protobuf-codegen.
2026-03-13 12:18:08 -07:00
Andrew
19e0b3d34f Reset for version v0.88.3 2026-03-13 11:43:31 -04:00
andrew-signal
77a04db08e
Revert "node: Stop statically linking the C runtime on Windows" 2026-03-13 11:40:51 -04:00
moiseev-signal
6c2bf65989
keytrans: Implement improved logic for monitor_and_search 2026-03-12 11:00:36 -07:00
andrew-signal
f7c4aceebd
follow-on: Clarify test naming in BackupJsonExporter tests 2026-03-11 16:19:14 -04:00
andrew-signal
aad9131f5d
ci: Increase Android emulator disk size in Slow Tests 2026-03-10 18:33:27 -04:00
andrew-signal
68019908f8
follow-on: Clean up BackupJsonExporter tests 2026-03-10 18:33:05 -04:00
andrew-signal
5efc009a63
java: Bridge BackupJsonExporter for takeout export 2026-03-10 14:15:57 -04:00
andrew-signal
9cf78b7509
Expose useH2ForAuthChat remote configuration option 2026-03-09 21:08:30 -04:00
andrew-signal
c8a9b64590
Remove disableNagleAlgorithm remote configuration option 2026-03-09 19:00:45 -04:00
andrew-signal
8607a91ef6
ci: Parameterize cache bucket name 2026-03-09 12:46:56 -04:00
andrew-signal
5c4c5435e2
node: Refactor internals of bridge's BackupJsonExport, while keeping same interface. 2026-03-07 06:13:06 -05:00
Jordan Rose
55b233d43c
ffi: Support multiple return values for bridge_callbacks
...and use it to avoid re-deriving the public key when fetching the
local identity key pair.
2026-03-06 12:01:41 -08:00
Max Moiseev
6a7cc67173 Reset for version v0.88.2 2026-03-06 11:54:36 -08:00
Jordan Rose
4bc22d2216
net: Apply chat WS headers to H2 requests on the same connection
Adds the notion of "per-request default headers" to our Http2Client,
then copies over the headers used to connect a chat websocket to use
for H2 requests---in practice, gRPC requests. This includes the user
agent, and will also include auth information for auth requests.
2026-03-06 10:43:20 -08:00
moiseev-signal
23918f2601
backups: Support access control for member labels 2026-03-06 10:25:11 -08:00
andrew-signal
39197348f0
java: Map ChatServiceInactive to RetryableNetworkError. 2026-03-06 11:54:32 -05:00
Jordan Rose
d604dbd076 bridge: Use bridge_callbacks for SessionStore in Java and Node 2026-03-05 15:50:01 -08:00
Jordan Rose
32bef826ac java: Declare internal stores as returning nullable values
...matching the #[bridge_callbacks] traits in protocol/storage.rs.
This is a place where the different platforms have historically
diverged, but given that the public-facing traits are declared in Java
and the Rust side can handle null, the glue code should not rule it
out.
2026-03-05 15:50:01 -08:00
Jordan Rose
d390508da5 java: Eliminate other uses of deprecated Gradle features
And add `--warning-mode fail` to the CI invocations to not regress.
2026-03-04 15:41:18 -08:00
Daeho Ro
06f30cb23d update build.gradles to support gradle v9 2026-03-04 15:41:18 -08:00
Jordan Rose
d7fcde47d6 chat: Add Backups APIs get_upload_form and get_media_upload_form 2026-03-03 17:50:00 -08:00
Jordan Rose
017c6ec8d3 chat: Introduce RequestError::flat_map_other helper 2026-03-03 17:50:00 -08:00
Jordan Rose
5e5f0ece94 Consistently import base64::prelude::Engine
It's also available as base64::Engine, but we're always using one of
the preset engines from base64::prelude in practice.
2026-03-03 17:50:00 -08:00
Jordan Rose
cdc158dfdb chat: Factor out expect_empty_body helper 2026-03-03 17:50:00 -08:00
Jordan Rose
86e4175ce1 Relax Sized bounds on Rngs in libsignal-core and libsignal-account-keys 2026-03-03 17:50:00 -08:00
moiseev-signal
5e97729155
backups: Update to latest backup.proto 2026-03-03 14:17:07 -08:00
Jordan Rose
83c83c36a7 Reset for version v0.88.1 2026-03-02 15:59:58 -08:00
Jordan Rose
44bd39743a CI: Include java-extra-bridging-checks in slow_tests' report-failures 2026-03-02 15:55:57 -08:00
Jordan Rose
d0d4fdde88 CI: Consistently use apt-get install -U
Shorter than `apt-get update && apt-get install`, and fixes one place
where we forgot to do that.
2026-03-02 15:55:57 -08:00
moiseev-signal
42f426fc94
keytrans: Validate tree_size and pos before tree math
Co-authored-by: Steve Weis <sweis@anthropic.com>
2026-03-02 14:29:52 -08:00
Jordan Rose
36ab93849a java: Default to linux/amd64 for Docker builds, not the host CPU
Apart from wanting a reproducible build, our apt mirrors only contain
amd64 packages.
2026-03-02 13:47:37 -08:00
Jordan Rose
83f1b335ab
chat: Add gRPC send_multi_recipient_message 2026-02-27 15:57:19 -08:00
Jordan Rose
3f92b94484 java: Add missing NoSessionException to combined sealed encrypt method
...and simplify the implementation to reuse SessionCipher.

The API using UnidentifiedSenderMessageContent is not affected.
2026-02-27 15:31:47 -08:00
andrew-signal
83ab6d3eec
java: Expose KeyTransparency return values as RequestResult. 2026-02-27 01:58:59 -05:00
gram-signal
f80e6cc647
Remove 2025Q3 SVRB from staging. 2026-02-26 16:26:05 -08:00
Jordan Rose
ea62515452 Rename internal.kt to Internal.kt
...per Kotlin conventions (even for files containing multiple classes).
Now we can remove the suppressed lint.
2026-02-26 13:41:08 -08:00
Jordan Rose
352d170876 bridge: Use bridge_callbacks for SenderKeyStore in Java and Node 2026-02-26 13:41:08 -08:00
Alex Lillo Lindo
cd28805329 Fix README packagingOptions deprecated warning
Fix packagingOptions deprecation warning when following the README documentation, changing it for packaging
2026-02-26 12:12:09 -08:00
Marc
bd4ec2bee1 Reset for version v0.87.6 2026-02-26 13:25:40 -05:00
Sasha Weiss
ffaa9f0435
Add IOSSpecificSettings to Backup.proto
Co-authored-by: Max Moiseev <moiseev@signal.org>
2026-02-25 17:00:49 -08:00
marc-signal
56e3330a9a
Treat HTTP/2 transport errors distinct from gRPC status 2026-02-25 19:12:57 -05:00
marc-signal
9c04f9b74c
Simplify use of try_from 2026-02-25 18:50:43 -05:00
marc-signal
d645c48aaa
Log the hashes of certs on error 2026-02-25 18:06:31 -05:00
Jordan Rose
db0152418a ffi: Emit ServerSideErrors from libsignal-net-chat as ioError
...not networkProtocolError, which generally indicates a bug where
retrying won't help. ioError isn't really apt, but it's an existing
error code the iOS app considers retryable.
2026-02-24 13:15:40 -08:00
andrew-signal
954306ac7f
Fix bug where exception in completion handler would cause CompletableFuture to hang. 2026-02-24 14:08:38 -05:00
Jordan Rose
46611def16
chat: Disallow RateLimitChallenges on Unauth connections 2026-02-24 10:02:12 -08:00
andrew-signal
c5845300e9
net: Add an affirmative log when we start a TLS proxy handshake 2026-02-23 14:12:34 -05:00
Jordan Rose
3a82ed8e0b chat: Improve test output when a gRPC req doesn't match expectations 2026-02-20 16:44:17 -08:00
gram-signal
1b27f27feb
Update SPQR to v1.5.0, libcrux-ml-kem and hpke-rs to latest.
Co-authored-by: Max Moiseev <moiseev@signal.org>
2026-02-20 15:51:14 -08:00
Jordan Rose
d25b710d62 Android: use packed symbol relocations, now that we're on API 23 2026-02-20 12:11:53 -08:00
Jordan Rose
2d3f4158ae node: impl<T> CallbackResultTypeInfo for Option<T>
This matches the existing impls for ArgTypeInfo and AsyncArgTypeInfo.
Unfortunately, we can't do the same for the C and JNI bridges, because
they `impl SimpleArgTypeInfo for Option<something>` and that would
conflict with this implementation of CallbackResultTypeInfo.
2026-02-20 10:00:01 -08:00
Jordan Rose
e5647239d8 bridge: Use bridge_callbacks for KyberPreKeyStore in Java and Node too 2026-02-20 09:45:28 -08:00
Jordan Rose
bed6f5e5ae Reset for version v0.87.5 2026-02-19 16:27:06 -08:00
Jordan Rose
38514bdc8a
Update to boring v5.0.2
The new version of BoringSSL requires a C++ library:
- Android: libc++ is linked statically, like WebRTC does
- Swift: libstdc++ is linked on Linux (and libz is dropped everywhere)
- Everywhere else: the system C++ library will be linked dynamically

And makes SSE2 a requirement for 32-bit Linux, matching 32-bit Android.
2026-02-19 15:26:34 -08:00
marc-signal
5bb92bfe13
Add RemoteConfig for AccountExists 2026-02-19 17:06:28 -05:00
andrew-signal
49513ab592
chat: Add keys service with getPreKeys 2026-02-19 14:40:28 -05:00
Max Moiseev
ba9f75383f Reset for version v0.87.4 2026-02-19 11:19:11 -08:00
Jon Chambers
b13f76c8dd Move 2025 Q3 SVRB enclave to "previous" 2026-02-19 13:24:37 -05:00
moiseev-signal
16be17b4c6
keytrans: Remove search-with-version fallback 2026-02-18 16:44:00 -08:00
moiseev-signal
c17dc6ca7b
java: Update Dokka to 2.1.0 2026-02-18 16:35:57 -08:00
marc-signal
e65993fdc2
Upgrade node version to match desktop 2026-02-18 16:10:12 -05:00
moiseev-signal
c489f3f393
Update nightly to 2026-02-11 2026-02-17 17:44:29 -08:00
moiseev-signal
67ea4cc351
backups: Include invalid admin id in the error message 2026-02-17 12:15:17 -08:00
Max Moiseev
cc3f031eaa Reset for version v0.87.3 2026-02-17 10:44:09 -08:00
Jordan Rose
1e1f808844 Remove some outdated readmes 2026-02-17 10:42:36 -08:00
moiseev-signal
12d487ffb9
backups: Support admin deleted messages 2026-02-17 08:57:06 -08:00
moiseev-signal
c0e19d7d4a
CI: Use a new ubuntu snapshot for both Java and Node docker builds 2026-02-13 17:01:49 -08:00
Jordan Rose
82f1633d6a node: Stop statically linking the C runtime on Windows
Originally we did this to support deploying to Windows 8 without an
extra dependency (the modern C runtime library on Windows was
introduced in Windows 10). However, Signal-Desktop no longer supports
Windows 8, and for that matter neither does Rust, so we might as well
use the DLL that's part of the OS instead.
2026-02-13 10:27:28 -08:00
andrew-signal
d1795c244b
Have TLS proxy users connect to grpc.chat.signal.org over HTTP/2, subject to same conditions as direct. 2026-02-12 12:28:53 -05:00
Jordan Rose
55b71ff92d Update to boring tag signal-4.21.1a
Fixes an issue cross-compiling for Android on macOS.
2026-02-11 15:12:09 -08:00
Jordan Rose
606072ab92 bridge: Use bridge_callbacks for SignedPreKeyStore in Java and Node too 2026-02-11 13:07:49 -08:00
Jordan Rose
bf48c34d23 bridge: Introduce CallbackResultTypeInfo for Node
The existing traits turn out to not quite be flexible enough after
all. We'll follow the other bridges instead and make things extra
clear.
2026-02-11 13:07:49 -08:00
moiseev-signal
203d8412a1
keytrans: Add metadata to StoredAccountData 2026-02-11 10:58:57 -08:00
Rolfe Schmidt
dce9c0d30a
Validate Diffie-Hellman agreements 2026-02-10 16:16:24 -07:00
Jordan Rose
9bbe720882 look_up_username: Extract apply_to_both helper 2026-02-10 13:30:24 -08:00
Jordan Rose
9e5f68e3fa bridge: Merge PreKeyStore traits across bridges 2026-02-10 11:22:42 -08:00
Jordan Rose
32b5163c2e Add remote config key "grpc.AccountsAnonymousLookupUsernameLink" 2026-02-09 16:17:50 -08:00
andrew-signal
2e6b194ca7
Bump to signalapp/boring v4.21.1 2026-02-09 18:52:31 -05:00
Jordan Rose
7908e504fa
chat: Implement gRPC lookUpUsernameLink
And add support for username links to the look_up_username example
2026-02-09 15:48:15 -08:00
Jordan Rose
ae3e04f978 Write down how to test the apps against local builds of libsignal 2026-02-09 15:36:15 -08:00
Jordan Rose
a0859c0268
bridge: Use bridge_callbacks for PreKeyStore (Node bridge)
Unlike the C and JNI bridges, we don't need a separate
CallbackResultTypeInfo, because async callbacks already can't borrow
from their arguments. The structure is otherwise largely the same.
2026-02-09 15:15:23 -08:00
Jordan Rose
8ca5252980 Note that libprotobuf-dev is required now 2026-02-09 09:41:40 -08:00
Jordan Rose
0e402a8c95 net: Default post_request_interface_check_timeout to 5sec
This has been set at 5sec for both Android and iOS for a long time,
with beneficial effects and no complaints.
2026-02-06 16:13:56 -08:00
marc-signal
58e916940c
Enable overflow checks for release builds 2026-02-06 17:37:14 -05:00
Max Moiseev
8406795667 Reset for version v0.87.2 2026-02-06 12:40:13 -08:00
moiseev-signal
f08390b0e2
Add an explicit dependency to address RUSTSEC-2026-0009 2026-02-06 11:45:34 -08:00
Jordan Rose
3d150964c2 Update bytes to 1.11.1 2026-02-04 16:49:35 -08:00
Jordan Rose
ac903f0d28 Fix -Zdirect-minimal-versions build 2026-02-04 16:49:35 -08:00
emir-signal
d73d695bb6
Make ROOT_KEY_MAX_BYTES_FOR_SHO public 2026-02-04 15:43:07 -05:00
Jordan Rose
91e36ddc64
libsignal-protocol-test-support: reusable bits of session fuzz test
Factored out for testing explicit scenarios as well.
2026-02-03 15:36:02 -08:00
emir-signal
79db90a5d8
Allow CallLinkRootKey to vary in size and remove call link epochs from backup 2026-02-03 11:42:34 -05:00
Jordan Rose
845c4e3fc2 Reset for version v0.87.1 2026-02-02 16:39:30 -08:00
marc-signal
ec3aa0827f
Update lock files 2026-02-02 14:01:52 -05:00
marc-signal
6dd67d52e8
Remove ordered public key comparison 2026-02-02 13:37:11 -05:00
Jordan Rose
e419b9cf01 Update Gradle, AGP, Kotlin versions 2026-01-30 17:14:36 -08:00
Jordan Rose
d7f99838a6 java: Manually install protoc 3.29 in Docker, like Desktop's Docker
Previously we'd had jammy's 3.12, but that's missing some things we
use. Newer releases exist, but this is the one we've been using in
node/Dockerfile for a while, so if there's a problem we should update
both.
2026-01-30 15:22:53 -08:00
Jordan Rose
cc9d60a8a8
chat: Allow WS-based connections to defer to gRPC instead
If the WS connection has an H2 channel, and the particular operation
is on the allowlist* snapshotted from RemoteConfig, the gRPC form of
the operation will be used instead.

This must be enabled for each operation explicitly; currently, only
look_up_username_hash is supported.

This removes (by permanently enabling) the "grpc" feature from 
libsignal-net-chat.

*Currently it functions as an allowlist, but it's set up to also be a
blocklist in the future.
2026-01-30 12:53:02 -08:00
Jordan Rose
6a9f73b998 bridge: Use bridge_callbacks for PreKeyStore (Java bridge)
Adds support for callbacks with results to the JNI bridge, similar to
what f2eafbe6f8 did for the C bridge.

This does require introducing an "internal" version of PreKeyStore
that has simpler signatures for its callbacks; previously, the Java
objects for bridge_handle types were constructed on the Rust side of
the bridge, but that's not how bridge_fn works, and so it's not how
bridge_callbacks works. The additional overhead should not be
prohibitive.
2026-01-29 15:50:16 -08:00
Jordan Rose
7e84d2d7d8 bridge: Add jni::CallbackResultTypeInfo
Similar to 81e47eb31 for the ffi bridge.
2026-01-29 15:50:16 -08:00
Jordan Rose
68c9a0616b bridge_callbacks: don't cfg-guard the trait itself
This allows for manual implementations.
2026-01-29 15:50:16 -08:00
Jordan Rose
f92938d492 net: Thread gRPC overrides down to ChatConnection (unused for now)
These are a libsignal-net-chat concept, but we need a place to store
them, so like the GrpcBody alias we put a little knowledge into the
libsignal-net layer. The next commit will expose that in a way that
can be used by libsignal-net-chat.
2026-01-29 15:49:48 -08:00
Jordan Rose
53d4f5a3fe bridge: Keep gRPC RemoteConfig keys in sync with service names 2026-01-29 15:49:48 -08:00
Jordan Rose
f8f821e9ea swiftlint: Check for public structs without explicit inits
These might be places where we've chosen not to expose an init, but
they might also be places where we forgot. (It's only structs because
classes don't synthesize initializers by default, other than
inheriting them.)

As a regex-based check, this isn't perfect; it's specifically looking
for "public" followed by "struct" followed by "{" followed by "}" with
no "init(" (or a few other variations) between the two braces. This
does not at all handle nesting, so if a struct has other, non-stored
members, they must come after at least one initializer.
2026-01-29 15:14:53 -08:00
marc-signal
a6edef3ad0
Add higher-level bridge code for account_exists() 2026-01-29 14:22:56 -05:00
Jordan Rose
f758cf9794 CI: Don't run Release Notes check for public PRs 2026-01-29 09:58:02 -08:00
moiseev-signal
6fdb2d5166
QoL: Add a useful hints to the release script 2026-01-29 09:36:30 -08:00
Andrew
70c00c8dc5 Reset for version v0.86.17 2026-01-29 12:26:29 -05:00
andrew-signal
69735455dd
Make E164Info and AciInfo constructors public 2026-01-29 11:49:59 -05:00
Jordan Rose
77f41bbaeb chat: Make gRPC failures directly convertible to RequestError
Previously this was an explicit function (rather than a From
implementation) to remind callers to check for any per-request
failures before running through the shared error extraction logic.
However, we've committed to representing all per-request failures
in-band in an OK response, leaving gRPC-level error codes for generic
errors only. So we don't need ceremony around the conversion anymore.
2026-01-28 17:32:17 -08:00
Max Moiseev
0c874690a0 Reset for version v0.86.16 2026-01-28 15:55:03 -08:00
moiseev-signal
1346a40e34
backups: Support key transparency fields 2026-01-28 15:06:07 -08:00
Jordan Rose
2d4b7b9b3b net: Simplify cfg guard for testing-only API 2026-01-28 13:34:59 -08:00
Jordan Rose
632d32c5fe net: Move over-broad import to testing submodules 2026-01-28 13:34:59 -08:00
Jordan Rose
479f044cd6
chat: Add standard handling for "grpc.chat.signal.org" errors 2026-01-28 13:34:35 -08:00
Jordan Rose
143ca1ce08 libsignal-net-grpc: Enumerate service methods as enum cases
These provide a set of constants that can identify gRPC calls.
2026-01-28 12:09:26 -08:00
Jordan Rose
618f6c96ab fuzz: Fix session_management message drop logic
Decryption error messages aren't recorded in the message send logs, so
we shouldn't NACK (or ACK) them.
2026-01-26 14:24:15 -08:00
marc-signal
48c1120601
Run boring pre-commit-check in unique target dir 2026-01-26 15:58:07 -05:00
marc-signal
53380fca8e
Add accountExists typed API 2026-01-26 15:56:52 -05:00
Ruben De Smet
b1281d60f9 refactor: validate_with_trust_roots without indirection
This refactors SenderCertificate::validate_with_trust_roots to take
`&[impl AsRef<PublicKey>]` instead of the forced double indirection
`&[&PublicKey]`.
This seems to have been introduced to be more FFI-friendly,
but is rather inconvenient to work with from the Rust-API side.

Using `&[AsRef<PublicKey>]` allows calling both with `&[PublicKey]`
and `&[&PublicKey]`.

For context, see https://github.com/whisperfish/libsignal-service-rs/pull/385
2026-01-26 12:09:38 -08:00
Jordan Rose
3c5e82b1c6 chat: Update gRPC protos, adjust look_up_username_hash to match
The protos are copied directly from the Signal-Server repo; any
changes should happen there first.
2026-01-23 11:53:32 -08:00
gram-signal
28ffbcaed1
Update SVR2/B to use new production backends. 2026-01-22 16:04:26 -08:00
marc-signal
c5ba215e3f
Only rebuild protos on change 2026-01-22 18:36:44 -05:00
Marc
9d9122c154 Reset for version v0.86.15 2026-01-22 16:18:51 -05:00
marc-signal
52f4f99c4c
Add backup validator for group member labels 2026-01-22 14:58:20 -05:00
Jordan Rose
85bf014f1d core: Add missing "derive" feature for zerocopy (from a24341b04) 2026-01-22 10:26:15 -08:00
marc-signal
f5ab0fe616
Upgrade to Rust 1.88 2026-01-21 15:39:02 -05:00
Jordan Rose
40955292f9 Update CODING_GUIDELINES (no major changes) 2026-01-21 11:39:46 -08:00
moiseev-signal
67e624edca
Remove unused remote config key 2026-01-16 17:34:53 -08:00
gram-signal
a982a115e4
SVRB: Use all provided instances in example. 2026-01-16 16:06:24 -08:00
Jordan Rose
88b7fe66fc fuzz: Add session_management test
Similar to the 'interactions' test, but including scenarios where
decryption failures are expected. The goal is to check that after a
period of potential instability, exchanging messages "normally" will
get both participants on the same session in a reasonable amount of
time.
2026-01-16 14:37:53 -08:00
Jordan Rose
3de0430b8b ts: Fix accidentally-lax typing for Net.setRemoteConfig 2026-01-16 14:35:02 -08:00
Max Moiseev
a95feb9c5d Reset for version v0.86.14 2026-01-16 10:32:47 -08:00
gram-signal
bc7f5719d4
Updates for SVR enclave release 2026Q1. 2026-01-15 16:33:00 -08:00
Jordan Rose
dff54c1af2 account-keys: Simplify a test helper to avoid explicit array lengths 2026-01-15 15:45:56 -08:00
Jordan Rose
0b712a1d0a Use zerocopy for parsing in keytrans and CDSI too 2026-01-15 15:45:56 -08:00
Jordan Rose
a24341b044 Add derive_arrays helper, for getting multiple buffers out of HKDF
(and similar derivations, like password hashing)

Packages up a zerocopy-based pattern originally added by Alex into
a more conventional interface.
2026-01-15 15:45:56 -08:00
emir-signal
bc7d2af953
Limit number of call link root key bytes used in CallLinkSecretParams derivation 2026-01-15 15:44:25 -08:00
Jordan Rose
b4bee1c214
swift: Remove NativeHandleOwner's "borrowed" state
Now that the stores no longer use this optimization, it's unnecessary
complexity in the implementation of NativeHandleOwner. We can always
pull it out of git history if necessary.
2026-01-14 17:26:28 -08:00
Jordan Rose
e11318dbdb
Switch remaining C stores over to bridge_callbacks
This removes a micro-optimization used by IdentityKeyStore to combine 
bool and enum results. We can live without it.
2026-01-14 17:25:04 -08:00
Jordan Rose
a7e0cd9be0 Reset for version v0.86.13 2026-01-14 16:52:24 -08:00
moiseev-signal
0480b16ac3
Update libcrux and spqr dependencies 2026-01-14 16:50:55 -08:00
Jordan Rose
d6b61b46d2 Add "useH2ForUnauthChat" remote config
When set, and when not using a transparent proxy, unauth chat
connections will be made over H2 instead of HTTP/1.1. Auth connections
will not be affected.
2026-01-14 13:50:53 -08:00
Jordan Rose
282fa41534 net: Add experimental H2 ConnectionConfig for chat 2026-01-14 13:50:53 -08:00
Jordan Rose
74cb077238 net: Don't verify DNS expectations for domains with no static IPs 2026-01-14 13:50:53 -08:00
Jordan Rose
f8ba33c1ab attest: Simplify test that only parses a prefix of input data 2026-01-12 15:07:25 -08:00
Jordan Rose
13649374f5 Comment where we'd like to use <[T]>::as_chunks 2026-01-12 15:07:25 -08:00
Jordan Rose
a1a5f0f51e usernames: Use split_at instead of manual measurement 2026-01-12 15:07:25 -08:00
Jordan Rose
73071eee1b Use *_chunk helpers instead of repeating array lengths when possible 2026-01-12 15:07:25 -08:00
Andrew
504af9001c Reset for version v0.86.12 2026-01-12 16:41:23 -05:00
Jordan Rose
c86b4a1f0c Fix -Zdirect-minimal-versions build by updating url and once-cell
...or rather, updating the direct requirement specs for those two.
This has no effect on what we build within the libsignal workspace;
the lockfile already uses more recent versions. The versions chosen
are the minimum required by our other recently-bumped dependencies.
2026-01-12 12:43:09 -08:00
andrew-signal
184b5b9000
Synchronize our verify_algorithm_prefs with BoringSSL's kVerifySignatureAlgorithms 2026-01-12 15:38:58 -05:00
Jordan Rose
f2eafbe6f8 bridge: Use bridge_callbacks for KyberPreKeyStore (C bridge only)
Adds support for return values and error results to #[bridge_callbacks],
and then provides the necessary extra *TypeInfo impls to support
KyberPreKeyStore.

This also drops the "lazy clone" optimization previously used by the
store, where a bridged handle owned by Rust can be "borrowed" by the
Swift side and then cloned if still alive when the callback is about
to complete. Now the value is cloned eagerly, and destroyed via Swift
regardless. This is unfortunate, but the Android bridge has never had
this optimization, and Android supports devices with worse
performance, so we should be able to live with it.
2026-01-09 14:20:32 -08:00
Jordan Rose
81e47eb31c ffi: Add new conversion trait CallbackResultTypeInfo
For the most part we'll reuse SimpleArgTypeInfo implementations, but
that doesn't cover everything we currently do with callbacks in the C
bridge. This trait will help /bridge/ the gap in converting more
callback traits to use #[bridge_callbacks].
2026-01-09 14:20:32 -08:00
Jordan Rose
50e2743229 bridge-macros: Classify whether result types use the Result type
Not used yet, but will be soon.
2026-01-09 14:20:32 -08:00
Jordan Rose
fc1de1e454
Update many Rust dependencies
In addition to a `cargo update`,

- asn1 0.23
- clap-stdin 0.8 (CLI only)
- const-str 1.0
- hickory-proto 0.25 (test-only)
- hpke-rs 0.5
- rcgen 0.14 (test-only)
- tungstenite 0.28

And keeping `home` at 0.5.11.
2026-01-09 12:27:59 -08:00
Jordan Rose
d9283bf53c Remove (direct) dependency on foreign-types
This was a workaround for an unexposed API in boring-rs.
2026-01-09 12:13:29 -08:00
Jordan Rose
5aad667adb README: Mention Python as a tool dependency 2026-01-09 11:59:41 -08:00
Jordan Rose
cb685ceb80 bridge_callbacks: All autogenerated C callbacks should return int
...to possibly signal an error. None of the current callbacks make use
of this, but it'll be needed soon, and it's easier to be consistent.
2026-01-08 15:02:54 -08:00
Jordan Rose
fce7213e40 cargo-audit: Acknowledge that bincode is unmaintained 2026-01-08 13:54:41 -08:00
Jordan Rose
1d8500621d bridge: Comment that ResultTypeInfo is also used for callback args 2026-01-07 13:18:14 -08:00
Jordan Rose
78ca29b493 bridge_callbacks: Automatically generate TS interface 2026-01-07 13:18:14 -08:00
Jordan Rose
636d6479c9 bridge_callbacks: Add Node support 2026-01-07 13:18:14 -08:00
Jordan Rose
37dda46866 bridge_callbacks: Add JNI support
This required some changes to jni::ResultTypeInfo, because we no
longer have tokens available for the JNI types of a bridged function
to use with the jni_args! macro. Instead, we record signatures for
each type that might appear as a callback argument and use
const_str::concat to put them together.

This does not currently attempt to autogenerate the Java interface on
the other side of the bridge; given the way gen_java_decl builds on
top of cbindgen, this is likely tricky but not impossible. Something
to possibly add later.
2026-01-07 13:02:45 -08:00
Jordan Rose
a06877a8e8 bridge: Remove unnecessary JNI listener wrapper types 2026-01-07 13:02:45 -08:00
Jordan Rose
ec420b3102
bridge: Add #[bridge_callbacks] for bridging traits
This first commit only supports the C bridge, and only callback
functions that don't return anything at that. But that's sufficient to
fully cover ChatListener and ProvisioningListener.
2026-01-07 12:41:23 -08:00
Jordan Rose
fe149679fa fuzz: send multiple messages at a time in interactions test
This lets a shorter input sequence have a greater chance of doing a PQ
ratchet.
2026-01-06 17:44:49 -08:00
Jordan Rose
70366e86c8 prepare_release: Release actions are *not* run on the private repo 2026-01-06 15:51:34 -08:00
Jordan Rose
d3a4a47177 Reset for version v0.86.11 2026-01-06 13:38:48 -08:00
Jordan Rose
4cd2c507d3 justfile: Separate out check-python 2026-01-06 13:36:54 -08:00
Jordan Rose
e551e2444a prepare_release: Pass the repo name to get_workflow_name_mapping 2026-01-06 13:36:54 -08:00
Jordan Rose
2abe7c1845 prepare_release: Print uncaught exceptions better when they happen 2026-01-06 13:36:54 -08:00
Jordan Rose
f629f724cb backup: Mention unknown fields in errors about missing oneofs
This helps distinguish "I forgot to write a field" from "a field was
added but the validator doesn't know about it yet".
2026-01-06 12:07:19 -08:00
Jordan Rose
bea4305cbf backups: Accept blocks/unblocks of the Release Notes chat 2026-01-06 11:48:36 -08:00
Jordan Rose
72fda058ae ffi: Use wrapper structs for UUIDs instead of bare [u8; 16]
C treats array arguments and array return values non-symmetrically.
We've worked around that up until now, but it's simpler to not worry
about it.
2025-12-23 14:26:35 -08:00
moiseev-signal
a777b8c520
CI: Use workflow ids and add more output in prepare_release.py 2025-12-19 16:27:46 -08:00
Jordan Rose
2006368c10 swift: Avoid an edge case where disconnection errors would be leaked 2025-12-18 12:13:27 -08:00
Jordan Rose
f2142df7c0 GitHub: consistent workflow names
- "Integration - *" for jobs we run by hand (Android integration and
  Slow Tests)
- "Release - *" for the release jobs specifically
- "[CI] *" for the jobs that run automatically on PRs and merges
- "[auto] *" for other jobs that run automatically

The reason for the two kinds of prefix is to make the automatic jobs
show up lower in GitHub's list of workflows, since we now have so many
the list gets truncated by default.
2025-12-17 16:18:57 -08:00
Jordan Rose
1c54596e66 Reset for version v0.86.10 2025-12-17 11:27:46 -08:00
Jordan Rose
cfaf27f3a2 bridge: impl ResultTypeInfo for DisconnectCause
Groundwork for making the callback trait implementations more uniform.
No functionality change.
2025-12-17 10:32:49 -08:00
Jordan Rose
64ba0fcf26 swift: Improve type-safety for internal NonNull helper 2025-12-16 17:46:45 -08:00
Jordan Rose
5b4fc4850b NetTest.ts: clean up arg expectation tests a bit 2025-12-16 17:46:28 -08:00
Jordan Rose
fbdafc0222 ChatServiceTest.java: clean up a bit 2025-12-16 17:46:10 -08:00
moiseev-signal
558bea8229
backups: Allow polls in 1:1 chats + new limits 2025-12-16 16:41:37 -08:00
Jordan Rose
40940e4c31
java: Expose Network.connectProvisioning and ProvisioningConnection 2025-12-15 16:17:43 -08:00
Jordan Rose
3103b37faf swift: Expose Net.connectProvisioning and ProvisioningConnection 2025-12-15 16:03:35 -08:00
andrew-signal
0ec05213b9
ci: Right size per-commit Node Windows worker 2025-12-15 15:11:24 -06:00
Jordan Rose
4a0afd11e5 attest: unwrap() -> expect() 2025-12-15 13:07:33 -08:00
Jordan Rose
46306b2b6b net: Remove now-unused timeout constants 2025-12-15 12:53:58 -08:00
Jordan Rose
55123ec3f3
node: Add ProvisioningConnection and Net.connectProvisioning 2025-12-15 12:52:47 -08:00
Jordan Rose
5d1433cedd bridge: Add dedicated PossibleCaptiveNetwork error
This only applies to chat connections (including registration), and
for the time being is specifically looking for self-signed
certificates. Even though self-signed certificates can occur on any
network access, there should always be a chat connection early enough
in the app lifetime to identify the problem. (Other connections will
still log the error appropriately.)
2025-12-12 17:26:00 -08:00
Jordan Rose
c9115d1955 net: Report self-signed certificates using "fallback errors"
Even though these are route-specific, they can indicate problematic
networks that we want to report more specifically.
2025-12-12 13:35:36 -08:00
Jordan Rose
869ae954bb net: Support "fallback error" reporting from route::connect
A fallback error will be returned instead of AllAttemptsFailed, but
only if there are no successes or fatal errors. This allows failures
on a particular route to be elevated to the failure for the entire
connection attempt---something we shouldn't do often, but occasionally
would benefit from.
2025-12-12 13:35:36 -08:00
Jordan Rose
2478642135 net: Replace use of ControlFlow with our own route::ErrorHandling
...so I can add a case in the next commit.
2025-12-12 13:35:36 -08:00
Jordan Rose
4d743dc447 net: impl From<TransportConnectError> for WebSocketServiceConnectError
This is similar to the existing impl for net::chat::ConnectError: a
transport error doesn't get to the point where we can treat it as a
rejection by the service, so it's okay to skip the checking provided
in WebSocketServiceConnectError::from_websocket_error.
2025-12-12 13:35:36 -08:00
Jordan Rose
382e138b92 net: Remove ConnectError::NoResolvedRoutes
We aren't actually producing this; we always return AllAttemptsFailed
instead. Having no routes should generally be clear enough from logs
that we don't need a separate case for it.
2025-12-12 12:15:01 -08:00
Jordan Rose
7e4c3a7e8f java: Fix a hypothetical leak of a ServerMessageAck
If an incoming message notification comes in after the Java connection
object or listener has been GC'd, the ack handle would be leaked; now
it's properly cleaned up even on early exit.
2025-12-10 16:53:36 -08:00
Max Moiseev
2d76abfc38 Reset for version v0.86.9 2025-12-10 15:27:13 -08:00
moiseev-signal
ace404879f
backups: Support pinned messages 2025-12-10 15:24:23 -08:00
Jordan Rose
cd9a196648 swift: Avoid a memcpy delivering message envelopes
And consistently use our Data.init(consuming:) for this pattern.
2025-12-08 15:20:53 -08:00
Max Moiseev
4c308862a4 Reset for version v0.86.8 2025-12-08 12:18:02 -08:00
Jordan Rose
3929708a59 net: Add an endpoint_path parameter to chat connection establishment 2025-12-05 15:03:48 -08:00
Jordan Rose
4906c42c5f chat_smoke_test: Allow forcing proxy fallback on or off for testing 2025-12-05 15:03:27 -08:00
Jordan Rose
85638b1fee Add a note at the top of the readme about the Maven repo change 2025-12-05 10:53:04 -08:00
Jordan Rose
6b4770bbf2 net: unwrap() -> expect() in all net crates 2025-12-04 17:46:05 -08:00
Jordan Rose
cd27e75694 net: Add ProvisioningEvent, an alternate to net::chat::ServerEvent
(but similarly convertible from the underlying ws::ListenerEvent)

A provisioning chat connection presents different events from a
"normal" chat connection, other than disconnection.
2025-12-04 17:11:19 -08:00
Jordan Rose
f91624ac14 net: Try to extract SSL certificate-specific failures when present
When present, these error codes are much better than the high-level
"SSL" failure we had before...but they're not always available.
2025-12-04 17:09:12 -08:00
Jordan Rose
cbf6220cd8
Update spqr, then update prost, tonic, libcrux-ml-kem 2025-12-04 13:19:21 -08:00
andrew-signal
8191a48f15
Expose configuration option to disable Nagle's algorithm to improve connection open performance 2025-12-04 15:11:45 -05:00
moiseev-signal
123fae5b45
ci: Use our Ubuntu Focal snapshot for node builds 2025-12-03 13:07:32 -08:00
Pete Walters
f39f510b56
Add allowSealedSenderFromAnyone to backup settings 2025-12-02 16:56:05 -06:00
Jordan Rose
f33376ac03 Reset for version v0.86.7 2025-12-01 12:05:35 -08:00
Jordan Rose
494495504f Publish Java libraries to GCS instead of Maven Central
Co-authored-by: Andrew <andrew@signal.org>
2025-12-01 11:22:44 -08:00
Jordan Rose
02d51454f2 Update hpke-rs to avoid old libcrux-sha3 version
(We don't actually use it, but it still shows up in the dependency
graph.)
2025-12-01 11:07:47 -08:00
Jordan Rose
f5a10a78f8
net: Save H2 client in ChatConnection if available
...so it can be used later to make gRPC requests even while the
websocket is also being used.
2025-11-21 15:49:31 -08:00
Jordan Rose
8f30203f3d Remove duplicated release note 2025-11-21 15:44:02 -08:00
Jordan Rose
1bf4a3c6f5
Prefer expect() to unwrap() in many crates
(and avoid either in a few cases)
2025-11-21 12:06:45 -08:00
Jordan Rose
a0b94cf25d net: Support websocket connections over H2 as well as HTTP/1.1 2025-11-21 11:05:29 -08:00
Jordan Rose
8cd1c0b12f net: Add StreamWithFixedTransportInfo, an AsyncDuplexStream wrapper 2025-11-21 11:05:29 -08:00
Jordan Rose
0800506d3d net: Require WebSocketTransportStream, not just AsyncDuplexStream
...to connect a websocket, where WebSocketTransportStream also
includes Connection and Debug (and static lifetime). This will be
necessary for H2 upgraded streams, where hyper is going to wrap the
underlying stream and erase its type.
2025-11-21 11:05:29 -08:00
andrew-signal
81499b2d93
ci: Skip restore on main, do clean build, push clean artifacts to cache 2025-11-20 19:38:35 -05:00
andrew-signal
c83cae8fd8
ci: Bump Build and Test macOS runners to M2 workers to match Slow Tests 2025-11-20 19:37:44 -05:00
moiseev-signal
ebc4724f6c
keytrans: Pace search requests 2025-11-19 15:39:09 -08:00
moiseev-signal
6554a83d18
keytrans: Perform search-with-version if self-monitor returns unexpectedly high versions 2025-11-19 11:17:48 -08:00
Jordan Rose
94f030cdb7 Doc comments for AES-CTR, AES-GCM, and AES-GCM-SIV APIs 2025-11-18 16:03:19 -08:00
Jordan Rose
9b63526808 net: Add fallback IPv6 addrs for SVR2 and SVR-B 2025-11-18 15:45:26 -08:00
Jordan Rose
f15981ae78 net: Remove permessage-deflate support 2025-11-18 10:50:50 -08:00
Jordan Rose
ddcbfa560b Swap out dev-dependency json5 for its fork serde_json5 2025-11-18 10:35:24 -08:00
Jordan Rose
95ee094c0f tests: Allow certain validation errors for corrupted certificates 2025-11-17 15:16:36 -08:00
Max Moiseev
de5bbb992b Reset for version v0.86.6 2025-11-17 13:02:03 -08:00
Jordan Rose
b39e93f1a5
net: Add http_version to HttpRouteFragment and ConnectionConfig
Setting this at the route level is a requirement that doesn't quite
match up with the "negotiation" part of ALPN. However, we currently
know exactly which HTTP version we'll use to connect to any given
server: HTTP/1.1, unless we specifically know in advance the server
supports H2. That's sufficient for our uses here.
2025-11-14 17:10:56 -08:00
Jordan Rose
8894050176
chat: Fix parsing of 409/410 responses to sendMultiRecipientMessage 2025-11-14 17:04:03 -08:00
Max Moiseev
89e3d4df8f Reset for version v0.86.5 2025-11-14 16:57:39 -08:00
moiseev-signal
5a64e17ed4
backups: Pull the latest backups proto in and update the tests 2025-11-14 16:11:38 -08:00
Jordan Rose
db7d35eddf net: fix copy/paste error in SVRB DomainConfig 2025-11-14 13:24:43 -08:00
Jordan Rose
03433189b6 net: Test ALPN mismatches 2025-11-13 11:16:42 -08:00
Jordan Rose
c0a2099948 net: Include IP version in a connection attempt's log tag 2025-11-13 11:14:47 -08:00
Andrew
42935e5c5a Reset for version v0.86.4 2025-11-11 19:14:30 -05:00
andrew-signal
de881ddde4
backups: Exporter now returns [Result<String, Error>] instead of a serialized JSON array object
Co-authored-by: Jordan Rose <jrose@signal.org>
2025-11-11 16:52:47 -05:00
Jordan Rose
92adc95346 net: Add a gRPC mode to the look_up_username example 2025-11-10 13:58:55 -08:00
andrew-signal
8c3e1bff2b
backups: Allow "tombstones" from view once messages in takeout exports 2025-11-10 16:19:41 -05:00
Max Moiseev
acbe6822c5 Reset for version v0.86.3 2025-11-10 12:37:04 -08:00
andrew-signal
2dcd1e0b79
backups: Validate username uniqueness
Co-authored-by: Jordan Rose <jrose@signal.org>
2025-11-10 14:20:40 -05:00
Jordan Rose
cc7a670e91
net: Separate a generic Http2Client from AggregatingHttp2Client
Http2Client adapts hyper's SendRequest for our needs;
AggregatingHttp2Client continues to act as it always has---except that
it can be slightly more efficient by using `&mut self`.

Nothing is yet using Http2Client directly, but gRPC will.
2025-11-10 11:08:30 -08:00
moiseev-signal
76cea46935
backups: Improve tooling and documentation for tests 2025-11-07 16:47:17 -08:00
moiseev-signal
bb7451a55c
backups: Unignore fields in AccountData/AccountSettings 2025-11-07 12:57:22 -08:00
Jordan Rose
ef0001108a Reset for version v0.86.2 2025-11-07 11:55:30 -08:00
Jordan Rose
3f0d2b1384 Revert "java: Publish to GitHub Packages instead of Maven Central"
This reverts commit b2c5685080.
2025-11-07 11:37:25 -08:00
Jordan Rose
8af11e52ea Reset for version v0.86.1 2025-11-06 17:50:19 -08:00
Jordan Rose
7475974889 ProGuard: keep kotlin.Pair's constructor 2025-11-06 17:17:49 -08:00
Jordan Rose
b2c5685080 java: Publish to GitHub Packages instead of Maven Central 2025-11-06 16:36:17 -08:00
Jordan Rose
11e8353843 node: Update minimum macOS version to 12 (Monterey) to match Desktop 2025-11-06 16:19:02 -08:00
Jordan Rose
776cf0601e Remove deprecated overloads of KyberPreKeyStore.markKyberPreKeyUsed 2025-11-06 16:10:09 -08:00
Jordan Rose
26f46d3d68
android: Bump minSdkVersion to 23 2025-11-06 15:51:06 -08:00
moiseev-signal
177a495eba
java: Use kotlin.Pair 2025-11-06 15:37:29 -08:00
Alex Bakon
1b2304022a
Expose net remote config keys in TypeScript
Co-authored-by: Max Moiseev <moiseev@signal.org>
2025-11-06 15:17:32 -08:00
andrew-signal
56ccf4c38b
ci: Bump to larger workers where it makes sense for release builders 2025-11-06 17:37:53 -05:00
andrew-signal
298dd979d7
ci: Add toggle to Slow Tests to make them faster for time sensitive workflows 2025-11-06 15:31:04 -05:00
andrew-signal
6289924c4c
Remove RemoteConfig:EnforceMinimumTls 2025-11-06 12:29:35 -08:00
Jordan Rose
6e0057636c
net: Shuffle static IPs, record all SVR2/B IPs
This serves two purposes:

- If we ever need the static DNS fallback, every client won't pick the
  same server to connect to.

- As long as the set of regions is up to date, regular DNS queries
  won't be warned about for returning an unexpected IP.
2025-11-06 11:46:05 -08:00
andrew-signal
cbbd9c3281
Revert "Add logging to BackupAuthCredentialRequestContext internals" 2025-11-05 22:17:57 -05:00
moiseev-signal
334e1e7962
backups: Enforce single vote in polls 2025-11-04 13:52:59 -08:00
andrew-signal
afbda11358
ci: Add checkbox to manual dispatch to skip cache restore/save 2025-11-04 12:50:22 -05:00
Jordan Rose
38edb37326 grpc: Strip tonic's features down to what we actually need 2025-11-04 09:40:24 -08:00
Jordan Rose
4154a0c2f0 Use env_logger consistently in CLI tools 2025-11-04 09:36:20 -08:00
andrew-signal
ff8baf8b33
ci: Skip cache push unless we're running against main 2025-11-04 00:05:41 -05:00
andrew-signal
ca5612f309
ci: Bump Build and Test's Windows Node job to windows-latest-8-cores 2025-11-03 23:30:43 -05:00
Andrew
be62a177b1 Reset for version v0.85.7 2025-11-03 23:25:12 -05:00
andrew-signal
e746f7a2c4
node: Add repository field to package.json
Authored-by: Max Moiseev <moiseev@signal.org>
2025-11-03 23:23:23 -05:00
Max Moiseev
663e406675 Reset for version v0.85.6 2025-11-03 16:24:39 -08:00
moiseev-signal
6736f8849d
Update node version to match Desktop 2025-11-03 15:30:17 -08:00
Andrew
55133b3d05 Reset for version v0.85.5 2025-11-03 15:29:19 -05:00
Jordan Rose
376318be63 CI: Run lints.yml when any workflow changes (for actionlint) 2025-11-03 11:23:13 -08:00
Jordan Rose
c3a3be50c2 Use NPM OIDC auth instead of an automation token 2025-11-03 11:22:16 -08:00
Pete Walters
a30a823817
Add 'Pin Reminders' setting 2025-11-03 11:28:46 -06:00
Max Moiseev
5e5ea1aad7 Reset for version v0.85.4 2025-10-31 16:40:01 -07:00
Pete Walters
3f8643cf44
backups: Add bioText/bioEmoji to backup AccountData
Co-authored-by: Max Moiseev <moiseev@signal.org>
2025-10-31 15:54:50 -07:00
Jordan Rose
8295ed3a5e
chat: Add a gRPC-based look_up_username_hash implementation 2025-10-30 14:50:56 -07:00
Jordan Rose
459550d512 net: Remove Noise Direct and Noise-over-Websocket implementations 2025-10-30 14:50:08 -07:00
Jordan Rose
f00ba1f25c CI: Add missing S3 env vars for rust-fuzz-build cache 2025-10-30 11:35:37 -07:00
Max Moiseev
0e99a6eb2d Reset for version v0.85.3 2025-10-29 16:45:24 -07:00
Jordan Rose
57f2254605 node: Use Node's assert rather than Chai's in FakeChat.ts
While this is only used for testing, it's still *present* in the built
app, and as such shouldn't be referring to devDependencies. Use the
ESLint rule 'import/no-extraneous-dependencies' to make sure this
doesn't slip in again.
2025-10-29 15:56:16 -07:00
Jordan Rose
977b95fc55 Add a script for skipping the build of BoringSSL
This is one of the slower parts of the build, and *occasionally* we
can get away without it, including when doing `cargo check
-Zdirect-minimal-versions`.
2025-10-29 15:00:17 -07:00
Jordan Rose
e335b8d425
net: Add ConnectionResources::connect_h2
Splits out the reusable parts of connect_ws, then exposes them for H2
connections as well. This only works because websockets and H2 share
their "transport route" (a TLS connection of some kind) and can thus
store their connection history in the same kind of attempts record.
2025-10-29 13:06:24 -07:00
moiseev-signal
bc50f80341
keytrans: Update tests to account for the server change 2025-10-29 10:40:25 -07:00
Jordan Rose
d95b4b5a57
Add shellcheck, flake8, and mypy to just check-pre-commit
And update mac_setup.sh to include mypy and flake8 and the new flake8
plugins.

Co-authored-by: Max Moiseev <moiseev@signal.org>
2025-10-28 15:05:47 -07:00
moiseev-signal
ce89b0387c
keytrans: Update the test data and enable integration tests in CI 2025-10-28 13:24:32 -07:00
Jordan Rose
68e764e03a net: ComposedConnector can always use the outer connector's Error
Previously, ComposedConnector had an error type as a generic argument,
which its inner and outer connectors had to be compatible with. In
practice, though, all of the error types for the outer connectors we
use are already errors that include a case for the inner connector
failing, so we can get rid of the flexibility of the generic argument
and make our use sites simpler.
2025-10-28 11:20:39 -07:00
Jordan Rose
9cba9f1baf backup_stats: Break out some more specific frame types 2025-10-28 10:01:33 -07:00
Jordan Rose
e77c332d01 net: Rework Http2Connector to be composable
...instead of containing its inner connector. This is more consistent
with other routes/connectors.
2025-10-27 16:57:07 -07:00
Jordan Rose
19f850ab3f backup: Add backup_stats example (quick and dirty statistics) 2025-10-27 15:04:36 -07:00
Jordan Rose
05a59c4269 zkgroup: Add Clone to every type that's already Serialize+Deserialize 2025-10-27 13:05:24 -07:00
Jordan Rose
23ceb9f04d bridge: Switch KT APIs over to libsignal-net-chat-style invocations
This lets us remove a temporary WsConnection impl that dropped log
info, at the cost of a more complicated call site and one extra heap
allocation.
2025-10-27 12:14:57 -07:00
andrew-signal
0d4b5c567e
ci: Pin cbindgen to current latest (v0.29.2) 2025-10-24 13:00:12 -04:00
Max Moiseev
c3ff263d47 Reset for version v0.85.2 2025-10-24 07:40:33 -07:00
788 changed files with 54380 additions and 22214 deletions

View File

@ -1,6 +1,6 @@
[advisories]
ignore = [
"RUSTSEC-2024-0370", # proc-macro-error is unmaintained, used by libcrux
"RUSTSEC-2024-0381", # pqcrypto-kyber is unmaintained
"RUSTSEC-2024-0436", # paste is unmaintained, used by libsignal-bridge
"RUSTSEC-2025-0141", # bincode is unmaintained, used by zkgroup
]

1
.cbindgen-version Normal file
View File

@ -0,0 +1 @@
0.29.2

View File

@ -7,5 +7,5 @@ disallowed-methods = [
{ path = "jni::JNIEnv::call_static_method_unchecked", reason = "use helper method instead" },
{ path = "jni::JNIEnv::new_object", reason = "use helper method instead" },
{ path = "jni::JNIEnv::new_object_unchecked", reason = "use helper method instead" },
".." # keep any defaults
]
]
allow-unwrap-in-tests = true

View File

@ -3,6 +3,9 @@ self-hosted-runner:
labels:
# Used in Slow Tests' AArch64 Linux Tests.
- ubuntu-24.04-arm64-4-cores
# This is... not a custom worker label, but it's not included in actionlint for some reason.
# See: https://github.com/rhysd/actionlint/blob/f9408506b4c7f9cda1263bca8166271f65e65c3d/rule_runner_label.go#L29
- windows-latest-4-cores
# Configuration variables in array of strings defined in your repository or
# organization. `null` means disabling configuration variables check.

View File

@ -18,6 +18,9 @@ outputs:
cache-key-current:
description: 'Hash of current working tree'
value: ${{ steps.calculate.outputs['cache-key-current'] }}
cache-key:
description: 'Full cache key used for cargo artifacts'
value: ${{ steps.calculate-primary-cache-key.outputs['cache-primary-key'] }}
runs:
using: 'composite'
steps:
@ -26,8 +29,15 @@ runs:
shell: bash
run: python3 "${{ github.action_path }}/calculate_cache_keys.py" --toolchain "${{ inputs.toolchain }}" >> "$GITHUB_OUTPUT"
- name: Calculate primary cache key
id: calculate-primary-cache-key
shell: bash
run: echo "cache-primary-key=${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base'] }}-${{ steps.calculate.outputs['cache-key-current'] }}" >> "$GITHUB_OUTPUT"
- name: Restore cargo cache
uses: runs-on/cache@3a15256b3556fbc5ae15f7f04598e4c7680e9c25 # v4
id: cache
if: ${{ env.DO_CLEAN_BUILD_AND_POPULATE_CACHE != 'true' }}
uses: runs-on/cache/restore@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
with:
# The special handling for the Windows target path comes because we overwrite
# $CARGO_BUILD_TARGET_DIR in build_node_bridge.py because Visual Studio's CLI
@ -45,6 +55,8 @@ runs:
# - We use the working tree hash as the final key component to ensure uniqueness per commit.
# - On cache miss, we fall back, in order, to:
# 1. Most recent cache from the last common ancestor with main.
# 1.1. Most recent cache from the last common ancestor with main's parent.
# 1.2. Most recent cache from the last common ancestor with main's grandparent.
# 2. Most recent cache for this job/OS/rustc combination.
# 3. Most recent cache for this job/OS combination.
# This yields perfect hits on reruns while still warming cold builds with close matches.
@ -52,5 +64,7 @@ runs:
key: ${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base'] }}-${{ steps.calculate.outputs['cache-key-current'] }}
restore-keys: |
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base'] }}-
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base-parent'] }}-
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base-grandparent'] }}-
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-
${{ inputs['job-name'] }}-${{ runner.os }}-

View File

@ -42,6 +42,17 @@ def get_merge_base() -> str:
return output
def get_merge_base_parent(commit: str) -> str:
"""Get the parent commit of the given commit."""
returncode, output, _ = run_command(
['git', 'rev-parse', f'{commit}^'],
check=False,
)
if returncode != 0 or not output:
return 'none'
return output
def get_working_tree_hash() -> str:
"""Get a hash representing the current working tree state."""
returncode, working_tree_hash, _ = run_command(
@ -109,16 +120,22 @@ def main() -> None:
rustc_version_hash = hashlib.sha256(rustc_version.encode()).hexdigest()[:32]
lca = get_merge_base()
lca_parent = get_merge_base_parent(lca)
lca_grandparent = get_merge_base_parent(lca_parent)
current = get_working_tree_hash()
print(f'rustc-version={rustc_version_hash}')
print(f'cache-key-merge-base={lca}')
print(f'cache-key-merge-base-parent={lca_parent}')
print(f'cache-key-merge-base-grandparent={lca_grandparent}')
print(f'cache-key-current={current}')
debug_parts = [
f'Toolchain={toolchain}',
f'RustcVersion={rustc_version}',
f'LCA={lca}',
f'LCAParent={lca_parent}',
f'LCAGrandparent={lca_grandparent}',
f'Current={current}',
]
print('Debug: ' + '; '.join(debug_parts), file=sys.stderr)

View File

@ -0,0 +1,23 @@
name: 'Save Cargo Cache'
description: 'Save cargo and build cache artifacts with appropriate keys'
inputs:
key:
description: 'The cache key to save under'
required: true
runs:
using: 'composite'
steps:
- name: Save cargo cache
if: ${{ env.DO_CLEAN_BUILD_AND_POPULATE_CACHE == 'true' }}
uses: runs-on/cache/save@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
with:
# Keep this path list in sync with restore-cargo-cache/action.yml.
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/registry/src
~/.cargo/git/db
~/.cargo/git/checkouts
target
${{ runner.os == 'Windows' && format('{0}\\libsignal', runner.temp) || '' }}
key: ${{ inputs.key }}

View File

@ -1,4 +1,4 @@
name: Android Integration Test
name: "Integration - Android"
on:
workflow_dispatch:
@ -23,13 +23,13 @@ jobs:
steps:
- name: Checkout libsignal
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: libsignal
submodules: recursive
- name: Checkout Signal-Android
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: signalapp/Signal-Android
ref: ${{ inputs.signal_android_branch }}
@ -63,7 +63,7 @@ jobs:
- name: Upload test results
if: failure()
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-results
path: |

View File

@ -1,10 +1,15 @@
name: Build and Test
name: "[CI] Build and Test"
on:
push:
branches: [ main ]
pull_request: # all target branches
workflow_dispatch: {}
workflow_dispatch:
inputs:
skip_cargo_cache:
description: Skip cargo cache restore/save steps
default: false
type: boolean
# On PRs, "head_ref" is defined and is consistent across updates. On
# pushes, it's not defined, so we use "run_id", which is unique across
@ -22,7 +27,8 @@ env:
# For dev builds, include limited debug info in the output. See
# https://doc.rust-lang.org/cargo/reference/profiles.html#debug
CARGO_PROFILE_DEV_DEBUG: limited
SHOULD_USE_CARGO_CACHE: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ENDPOINT != '' }}
DO_CLEAN_BUILD_AND_POPULATE_CACHE: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
SHOULD_USE_CARGO_CACHE: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ENDPOINT != '' && secrets.R2_BUCKET_NAME != '' && inputs.skip_cargo_cache != true }}
jobs:
changes:
@ -43,11 +49,12 @@ jobs:
rust_ios: ${{ steps.filter.outputs.rust_ios }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0
- uses: dorny/paths-filter@9d7afb8d214ad99e78fbd4247752c4caed2b6e4c # v4.0
id: filter
with:
filters: |
@ -131,7 +138,7 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
@ -150,7 +157,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: rust-${{ matrix.version }}
@ -174,11 +181,23 @@ jobs:
if: matrix.version == 'nightly'
- name: Rust docs
run: cargo +${{ matrix.toolchain }} doc --workspace --all-features --keep-going
run: cargo +${{ matrix.toolchain }} doc --workspace --all-features --no-deps --document-private-items --keep-going
if: matrix.version == 'stable'
env:
RUSTDOCFLAGS: -D warnings
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
rust32:
name: Rust (32-bit testing)
@ -202,11 +221,11 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib
- run: sudo apt-get install -U gcc-multilib g++-multilib
- name: Install protoc
run: ./bin/install_protoc_linux
@ -223,7 +242,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: rust32-${{ matrix.version }}
@ -232,6 +251,21 @@ jobs:
- name: Run tests (32-bit)
# Exclude signal-neon-futures because those tests run Node
run: cargo +${{ matrix.toolchain }} test --workspace --all-features --verbose --target i686-unknown-linux-gnu --exclude signal-neon-futures --no-fail-fast -- --include-ignored
env:
CFLAGS: "-msse2" # for BoringSSL
CXXFLAGS: "-msse2" # for BoringSSL
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
rust-fuzz-build:
name: Rust (Fuzz Targets)
@ -243,7 +277,7 @@ jobs:
if: ${{ needs.changes.outputs.rust == 'true' }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
@ -261,7 +295,14 @@ jobs:
- name: Restore cargo cache
id: rust-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/restore-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: rust-fuzz-build
toolchain: ${{ steps.rust-fuzz-build-toolchain.outputs.latest-stable-msrv-toolchain }}
@ -281,6 +322,18 @@ jobs:
env:
RUSTFLAGS: --cfg fuzzing
- name: Save cargo cache
uses: ./.github/actions/save-cargo-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
rust-fmt:
name: Rust (Formatting and Acknowledgments)
@ -293,7 +346,7 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
@ -310,7 +363,7 @@ jobs:
run: rustup toolchain install "${{ steps.rust-fmt-toolchain.outputs.pinned-nightly-toolchain }}" --profile minimal --component rustfmt
- name: Cache locally-built tools
uses: runs-on/cache@3a15256b3556fbc5ae15f7f04598e4c7680e9c25 # v4
uses: runs-on/cache@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
with:
path: local-tools
key: local-tools-${{ runner.os }}-infra-${{ hashFiles('acknowledgments/cargo-about-version', '.taplo-cli-version') }}
@ -337,7 +390,7 @@ jobs:
working-directory: rust/protocol/cross-version-testing
- name: Check acknowledgments
run: PATH="$PATH:$PWD/local-tools/bin" ./bin/regenerate_acknowledgments.sh && git diff --exit-code acknowledgments
run: PATH="$PATH:$PWD/local-tools/bin" ./bin/regenerate_acknowledgments.sh --check
java_android:
name: Java Android
@ -358,7 +411,7 @@ jobs:
steps:
- run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
# Download all commits so we can search for the merge base with origin/main.
@ -373,6 +426,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-linux-android,armv7-linux-androideabi
- name: Restore cargo cache
@ -383,12 +438,12 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: java-android
- run: ./gradlew --dependency-verification strict :android:build :android:assembleAndroidTest :android:lintDebug :android:packaging-test:assembleDebugAndroidTest :android:benchmarks:assembleReleaseAndroidTest -PandroidArchs=arm,arm64 -x :makeJniLibrariesDesktop | tee ./gradle-output.txt
- run: ./gradlew --dependency-verification strict --warning-mode fail :android:build :android:assembleAndroidTest :android:lintDebug :android:packaging-test:assembleDebugAndroidTest :android:benchmarks:assembleReleaseAndroidTest -PandroidArchs=arm,arm64 -x :makeJniLibrariesDesktop | tee ./gradle-output.txt
working-directory: java
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
@ -402,6 +457,18 @@ jobs:
- run: grep -v -F '***' ./check_code_size-output.txt >> "$GITHUB_STEP_SUMMARY"
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
java_jvm:
name: Java JVM
@ -419,7 +486,7 @@ jobs:
steps:
- run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
@ -441,7 +508,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: java-jvm
@ -449,9 +516,21 @@ jobs:
- name: Verify that the JNI bindings are up to date
run: rust/bridge/jni/bin/gen_java_decl.py --verify
- run: ./gradlew --dependency-verification strict build -PskipAndroid
- run: ./gradlew --dependency-verification strict --warning-mode fail build -PskipAndroid
working-directory: java
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
node:
name: Node
@ -459,7 +538,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest-4-cores, windows-latest, macos-15]
os: [ubuntu-latest-4-cores, windows-latest-4-cores, macos-15-xlarge]
needs: changes
@ -468,7 +547,7 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
@ -482,7 +561,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: node
@ -500,17 +579,17 @@ jobs:
if: startsWith(matrix.os, 'ubuntu-')
- run: choco install protoc
if: matrix.os == 'windows-latest'
if: startsWith(matrix.os, 'windows-')
- run: brew install protobuf
if: startsWith(matrix.os, 'macos-')
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
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
@ -533,6 +612,18 @@ jobs:
- run: npm run test
working-directory: node
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
swift_package:
name: Swift Package
@ -545,7 +636,7 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
@ -559,7 +650,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: swift-package
@ -569,7 +660,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: cargo +stable install cbindgen
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
- run: swift/verify_error_codes.sh
@ -584,6 +675,18 @@ jobs:
run: swift run -v Benchmarks --allow-debug-build
working-directory: swift/Benchmarks
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
# Disabled for now, broken on Linux in the Swift 6.0 release.
# See https://forums.swift.org/t/generate-documentation-failing-for-swift-6-pre-release/74534
# - name: Build Swift package documentation
@ -593,7 +696,7 @@ jobs:
swift_cocoapod:
name: Swift CocoaPod
runs-on: macos-15
runs-on: macos-15-xlarge
needs: changes
@ -603,9 +706,11 @@ jobs:
env:
LIBSIGNAL_TESTING_ONLY_ACTIVE_ARCH: 1
# For Swift 6.2. Can be removed when advancing to the macos-26 runner.
DEVELOPER_DIR: /Applications/Xcode_26.3.app
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
@ -629,7 +734,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: libsignal-ci-cache
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: swift-cocoapod
@ -641,3 +746,17 @@ jobs:
- name: Run pod lint
run: pod lib lint --verbose --platforms=ios --skip-tests
env:
LIBSIGNAL_TESTING_DISABLE_EXPLICIT_MODULES: 1
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}

View File

@ -1,4 +1,4 @@
name: Check Versions
name: "[CI] Check Versions"
# We want to run this job on all changes, so that we do not have to risk breakage slipping
# through due to the set of files included in the version consistency check getting out of sync
# with the set of files checked by the test dispatch logic.
@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive

View File

@ -1,4 +1,4 @@
name: Docs
name: "[CI] Docs"
env:
MDBOOK_VERSION: "0.4.43"
@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive

View File

@ -1,4 +1,4 @@
name: Build iOS Artifacts
name: "Release - iOS"
on:
workflow_dispatch:
@ -22,24 +22,24 @@ jobs:
# Needed for google-github-actions/auth.
id-token: 'write'
runs-on: macos-15
runs-on: macos-15-xlarge
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Checking run eligibility
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const dryRun = ${{ inputs.dry_run }};
const refType = '${{ github.ref_type }}';
const refName = '${{ github.ref_name }}';
console.log(dryRun
? `Running in 'dry run' mode on '${refName}' ${refType}`
console.log(dryRun
? `Running in 'dry run' mode on '${refName}' ${refType}`
: `Running on '${refName}' ${refType}`);
if (refType !== 'tag' && !dryRun) {
core.setFailed("the action should either be launched on a tag or with a 'dry run' switch");
@ -75,18 +75,18 @@ jobs:
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
- name: Attach artifact to the run
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
path: ${{ steps.archive-name.outputs.name }}
name: libsignal-client-ios
- uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
if: ${{ !inputs.dry_run }}
with:
workload_identity_provider: 'projects/741367068918/locations/global/workloadIdentityPools/github/providers/github-actions'
service_account: 'github-actions@signal-build-artifacts.iam.gserviceaccount.com'
- uses: google-github-actions/upload-cloud-storage@22121cd842b0d185e042e28d969925b538c33d77 # v2.1.0
- uses: google-github-actions/upload-cloud-storage@6397bd7208e18d13ba2619ee21b9873edc94427a # v3.0.0
if: ${{ !inputs.dry_run }}
with:
path: ${{ steps.archive-name.outputs.name }}
@ -94,7 +94,7 @@ jobs:
# This step is expected to fail if not run on a tag.
- name: Upload checksum to release
uses: ncipollo/release-action@66b1844f0b7ef940787c9d128846d5ac09b3881f # v1.14
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20
if: ${{ !inputs.dry_run }}
with:
allowUpdates: true

View File

@ -1,4 +1,4 @@
name: Upload Java libraries to Sonatype
name: "Release - Java"
run-name: ${{ github.workflow }} (${{ github.ref_name }})
on:
@ -21,22 +21,22 @@ jobs:
strategy:
matrix:
os: [windows-latest, macos-15]
os: [windows-latest-8-cores, macos-15-xlarge]
include:
- os: windows-latest
- os: macos-15
- os: windows-latest-8-cores
- os: macos-15-xlarge
additional-rust-target: x86_64-apple-darwin
# Ubuntu binaries are built using Docker, below
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Checking run eligibility
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const dryRun = ${{ inputs.dry_run }};
@ -61,7 +61,7 @@ jobs:
shell: cmd
- run: choco install protoc
if: matrix.os == 'windows-latest'
if: startsWith(matrix.os, 'windows-')
- run: brew install protobuf
if: startsWith(matrix.os, 'macos-')
@ -87,7 +87,7 @@ jobs:
CARGO_BUILD_TARGET: ${{ matrix.additional-rust-target }}
- name: Upload client libraries
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libsignal-client libraries (${{matrix.os}})
path: |
@ -95,7 +95,7 @@ jobs:
java/client/src/main/resources/*.dylib
- name: Upload server libraries
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libsignal-server libraries (${{matrix.os}})
path: |
@ -109,15 +109,15 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
- run: sudo apt-get install -U protobuf-compiler
- run: cargo +stable install cbindgen
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
- name: Verify that the JNI bindings are up to date
run: rust/bridge/jni/bin/gen_java_decl.py --verify
@ -125,26 +125,31 @@ jobs:
publish:
name: Build for production and publish
runs-on: ubuntu-latest-4-cores
permissions:
contents: read
# Needed for google-github-actions/auth.
id-token: write
runs-on: ubuntu-latest-8-cores
needs: [build, verify-rust]
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Download built client libraries
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: java/client/src/main/resources
pattern: libsignal-client*
merge-multiple: true
- name: Download built server libraries
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: java/server/src/main/resources
pattern: libsignal-server*
@ -156,31 +161,38 @@ jobs:
- name: Upload libsignal-android
if: ${{ inputs.dry_run }}
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libsignal-android
path: java/android/build/outputs/aar/libsignal-android-release.aar
- name: Upload libsignal-client
if: ${{ inputs.dry_run }}
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libsignal-client
path: java/client/build/libs/libsignal-client-*.jar
- name: Upload libsignal-server
if: ${{ inputs.dry_run }}
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libsignal-server
path: java/server/build/libs/libsignal-server-*.jar
- id: gcp-auth
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
if: ${{ !inputs.dry_run }}
with:
workload_identity_provider: 'projects/741367068918/locations/global/workloadIdentityPools/github/providers/github-actions'
service_account: 'github-actions@signal-build-artifacts.iam.gserviceaccount.com'
token_format: 'access_token'
- run: make publish_java
if: ${{ !inputs.dry_run }}
working-directory: java
env:
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USER }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
CLOUDSDK_AUTH_ACCESS_TOKEN: ${{ steps.gcp-auth.outputs.access_token }}
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEYID }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
# ASCII-armored PGP secret key

View File

@ -1,13 +1,13 @@
name: Lints
name: "[CI] Lints"
# This is in a separate job because we have shell scripts scattered across all our targets,
# *and* some of them have common dependencies.
on:
push:
branches: [ main ]
paths: ['**/*.sh', '**/*.py', '.github/workflows/lints.yml']
paths: ['**/*.sh', '**/*.py', '.github/workflows/*.yml']
pull_request:
paths: ['**/*.sh', '**/*.py', '.github/workflows/lints.yml']
paths: ['**/*.sh', '**/*.py', '.github/workflows/*.yml']
jobs:
lint:
@ -16,10 +16,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: sudo apt-get update && sudo apt-get install python3-flake8 python3-flake8-comprehensions python3-flake8-deprecated python3-flake8-import-order python3-flake8-quotes python3-mypy
- run: sudo apt-get install -U python3-flake8 python3-flake8-comprehensions python3-flake8-deprecated python3-flake8-import-order python3-flake8-quotes python3-mypy
- run: |
shopt -s globstar
shellcheck -- **/*.sh bin/verify_duplicate_crates bin/adb-run-test

View File

@ -1,4 +1,4 @@
name: Publish to NPM
name: "Release - NPM"
on:
workflow_dispatch:
@ -24,23 +24,23 @@ jobs:
strategy:
matrix:
os: [windows-latest, macos-15]
os: [windows-latest-8-cores, macos-15-xlarge]
include:
- os: macos-15
- os: macos-15-xlarge
rust-cross-target: x86_64-apple-darwin
- os: windows-latest
- os: windows-latest-8-cores
rust-cross-target: aarch64-pc-windows-msvc
# Ubuntu binaries are built using Docker, below
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Checking run eligibility
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const dryRun = ${{ inputs.dry_run }};
@ -67,14 +67,14 @@ jobs:
- run: brew install protobuf
if: startsWith(matrix.os, 'macos')
- run: cargo +stable install dump_syms --no-default-features --features cli
- run: cargo +stable install dump_syms --locked --no-default-features --features cli
- name: Get Node version from .nvmrc
id: get-nvm-version
shell: bash
run: echo "node-version=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.24
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: '.nvmrc'
@ -97,13 +97,13 @@ jobs:
shell: bash
- name: Upload library
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libsignal_client (${{matrix.os}})
path: node/prebuilds/*
- name: Upload debug info
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Debug info (${{matrix.os}})
path: |
@ -113,25 +113,25 @@ jobs:
build-docker:
name: Build (Ubuntu via Docker)
runs-on: ubuntu-latest
runs-on: ubuntu-latest-8-cores
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: node/docker-prebuildify.sh
- name: Upload library
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libsignal_client (ubuntu-docker)
path: node/prebuilds/*
- name: Upload debug info
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Debug info (ubuntu-docker)
path: |
@ -145,23 +145,25 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
- 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
permissions:
# Required for OIDC
id-token: write
# Needed for ncipollo/release-action.
contents: 'write'
contents: write
runs-on: ubuntu-latest
@ -170,29 +172,32 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org/'
- name: Download built libraries
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: libsignal_client*
path: node/prebuilds
merge-multiple: true
- name: Download debug info
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: Debug info*
path: debuginfo
merge-multiple: true
- name: Update npm
run: npm install -g npm@latest
- run: npm ci
working-directory: node
@ -212,12 +217,10 @@ jobs:
- run: npm publish --tag '${{ github.event.inputs.npm_tag }}' --access public ${{ inputs.dry_run && '--dry-run' || ''}}
working-directory: node
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# This step is expected to fail if not run on a tag.
- name: Upload debug info to release
uses: ncipollo/release-action@66b1844f0b7ef940787c9d128846d5ac09b3881f # v1.14
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20
if: ${{ !inputs.dry_run }}
with:
allowUpdates: true

View File

@ -1,4 +1,4 @@
name: Check release notes
name: "[CI] Check release notes"
# This is in a separate job because it only runs on pull requests and triggers
# on label changes in addition to code changes.
@ -13,8 +13,8 @@ env:
jobs:
check:
name: Check for release notes
# Don't check draft PRs
if: github.event.pull_request.draft == false
# Only check non-draft PRs in Signal's private repo.
if: (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private') && github.event.pull_request.draft == false)
runs-on: ubuntu-latest

View File

@ -1,4 +1,4 @@
name: Slow Tests
name: "Integration - Slow Tests"
env:
ANDROID_NDK_VERSION: 28.0.13004108
@ -26,17 +26,21 @@ on:
ignore_kt_tests:
type: boolean
description: 'Skip Key Transparency tests (sets LIBSIGNAL_TESTING_IGNORE_KT_TESTS)'
default: true
default: false
bigger_workers:
type: boolean
description: 'Run on larger, more expensive workers for faster results'
default: false
jobs:
java-docker:
name: Java (Docker)
runs-on: ubuntu-latest-4-cores
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-8-cores' || 'ubuntu-latest-4-cores' }}
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 60
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
@ -47,19 +51,19 @@ jobs:
echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=${LIBSIGNAL_TESTING_IGNORE_KT_TESTS:-<unset>}"
- run: make -C java
- name: Upload JNI libraries
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: jniLibs
path: java/android/src/main/jniLibs/*
retention-days: 2
- name: Upload full JARs
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: jars
path: java/*/build/libs/*
retention-days: 2
- name: Upload full AARs
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: aars
path: java/android/build/outputs/aar/*
@ -67,12 +71,12 @@ jobs:
java-docker-secondary:
name: Java (Secondary Docker)
runs-on: ubuntu-latest-4-cores
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-8-cores' || 'ubuntu-latest-4-cores' }}
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 60
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
@ -80,13 +84,13 @@ jobs:
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- run: make -C java
- name: Upload full JARs
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: jars-secondary
path: java/*/build/libs/*
retention-days: 2
- name: Upload full AARs
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: aars-secondary
path: java/android/build/outputs/aar/*
@ -100,22 +104,22 @@ jobs:
steps:
- name: Download jars
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jars
path: a/jars/
- name: Download jars (secondary)
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jars-secondary
path: b/jars/
- name: Download aars
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: aars
path: a/aars/
- name: Download aars (secondary)
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: aars-secondary
path: b/aars/
@ -128,14 +132,14 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- run: sudo apt-get install protobuf-compiler
- run: sudo apt-get install -U protobuf-compiler
- run: ./gradlew :client:test :server:test -PskipAndroid -PjniTypeTagging -PjniCheckAnnotations
working-directory: java
@ -143,7 +147,7 @@ jobs:
android-emulator-tests:
name: Android Emulator Tests
# For hardware acceleration; see https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
runs-on: ubuntu-latest-4-cores
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-8-cores' || 'ubuntu-latest-4-cores' }}
env:
# Proxy server tests will fail on Android API prior to 28
LIBSIGNAL_TESTING_PROXY_SERVER: ${{ fromJSON(matrix.api_level) >= 28 && secrets.LIBSIGNAL_TESTING_PROXY_SERVER || '' }}
@ -154,12 +158,12 @@ jobs:
strategy:
fail-fast: false
matrix:
# 21 is our minimal API level
# 23 is our minimal API level
# 33 is our target API level
include:
- api_level: 21
- api_level: 23
arch: x86
- api_level: 21
- api_level: 23
arch: x86_64
- api_level: 33
arch: x86_64
@ -178,20 +182,20 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Download JNI libraries
id: download
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jniLibs
path: java/android/src/main/jniLibs/
# From reactivecircus/android-emulator-runner
- name: AVD cache
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
id: avd-cache
with:
path: |
@ -201,23 +205,23 @@ jobs:
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
with:
arch: ${{ matrix.arch }}
api-level: ${{ matrix.api_level }}
ndk: ${{ env.ANDROID_NDK_VERSION }}
force-avd-creation: false
disk-size: 1024M
disk-size: 4096M
emulator-options: -no-window -noaudio -no-boot-anim -no-metrics
script: echo "Generated AVD snapshot for caching."
- name: Run tests
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
with:
arch: ${{ matrix.arch }}
api-level: ${{ matrix.api_level }}
ndk: ${{ env.ANDROID_NDK_VERSION }}
disk-size: 1024M
disk-size: 4096M
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -noaudio -no-boot-anim -no-metrics
script: |
@ -234,7 +238,7 @@ jobs:
- name: Upload logcat logs
if: always()
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: logcat-logs-api${{ matrix.api_level }}-${{ matrix.arch }}
path: java/logcat.log
@ -247,14 +251,14 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
- run: sudo apt-get install -U protobuf-compiler
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
@ -269,12 +273,12 @@ jobs:
node-docker:
name: Node (Ubuntu via Docker)
runs-on: ubuntu-latest
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-4-cores' || 'ubuntu-latest' }}
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
@ -286,7 +290,7 @@ jobs:
env:
PREBUILDS_ONLY: 1
- name: Upload prebuilds
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: node-prebuilds
path: node/prebuilds
@ -294,17 +298,17 @@ jobs:
node-docker-secondary:
name: Node (Secondary Ubuntu via Docker)
runs-on: ubuntu-latest
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-4-cores' || 'ubuntu-latest' }}
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: node/docker-prebuildify.sh
- name: Upload prebuilds
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: node-prebuilds-secondary
path: node/prebuilds
@ -318,12 +322,12 @@ jobs:
steps:
- name: Download prebuilds
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: node-prebuilds
path: a/prebuilds/
- name: Download prebuilds (secondary)
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: node-prebuilds-secondary
path: b/prebuilds/
@ -336,7 +340,7 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-pc-windows-msvc
@ -351,7 +355,7 @@ jobs:
id: get-nvm-version
shell: bash
run: echo "node-version=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: '.nvmrc'
- name: Build for arm64
@ -364,14 +368,15 @@ jobs:
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 45
# Uncomment this to select a specific version of Xcode to build and test with. Check the runner
# Selects a specific version of Xcode to build and test with. Check the runner
# image (in https://github.com/actions/runner-images/) to see which ones are available. You can
# also set this on specific steps if necessary.
# env:
# DEVELOPER_DIR: /Applications/Xcode_16.2.app
env:
# For Swift 6.2. Can be commented out again when the default runner moves to macos-26.
DEVELOPER_DIR: /Applications/Xcode_26.3.app
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
@ -401,6 +406,8 @@ jobs:
# We run this for the non-hermetic tests; it's otherwise the same as regular CI.
- name: Run pod lint
run: pod lib lint --verbose --platforms=ios
env:
LIBSIGNAL_TESTING_DISABLE_EXPLICIT_MODULES: 1
# Make sure we can build for device, just for completeness.
- name: Set up testing workspace
@ -422,14 +429,14 @@ jobs:
timeout-minutes: 45
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib protobuf-compiler
- run: sudo apt-get install -U gcc-multilib g++-multilib protobuf-compiler
- run: rustup +stable target add i686-unknown-linux-gnu
@ -469,6 +476,8 @@ jobs:
run: cargo +stable test --workspace --all-features --verbose --target i686-unknown-linux-gnu --exclude signal-neon-futures --no-fail-fast -- --include-ignored
env:
RUST_LOG: debug
CFLAGS: "-msse2" # for BoringSSL
CXXFLAGS: "-msse2" # for BoringSSL
- name: Run libsignal-protocol cross-version tests
run: cargo +stable test --no-fail-fast
@ -497,16 +506,16 @@ jobs:
FUZZ_JOBS: 4 # because this is a "4-cores" runner
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
- run: sudo apt-get install -U protobuf-compiler
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- name: Cache cargo-fuzz
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: local-tools
key: ${{ runner.os }}-fuzzing-local-tools-${{ env.CARGO_FUZZ_VERSION }}
@ -524,10 +533,13 @@ jobs:
- run: cargo fuzz build sealed_sender_v2 && cargo fuzz run sealed_sender_v2 -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
working-directory: rust/protocol
- run: cargo fuzz build session_management && cargo fuzz run session_management -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
working-directory: rust/protocol
- run: cargo fuzz build dcap && cargo fuzz run dcap -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
working-directory: rust/attest
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: failure()
with:
name: fuzzing-artifacts-${{ github.sha }}
@ -535,12 +547,23 @@ jobs:
# This isn't fuzzing, but we have to do it with a nightly compiler, so we're going to tack it on to this job.
- name: Build everything with no lockfile and -Zdirect-minimal-versions
run: mkdir minimal-versions && cargo check --workspace --all-targets --all-features --verbose --keep-going -Zdirect-minimal-versions -Zunstable-options --lockfile-path minimal-versions/Cargo.lock
run: mkdir minimal-versions && CARGO_RESOLVER_LOCKFILE_PATH=minimal-versions/Cargo.lock bin/without_building_boring.sh cargo check --workspace --all-targets --all-features --verbose --keep-going -Zdirect-minimal-versions -Zlockfile-path
report_failures:
report-failures:
name: Report Failures
runs-on: ubuntu-latest
needs: [java-docker, java-reproducibility, android-emulator-tests, aarch64, node-docker, node-reproducibility, node-windows-arm64, swift-cocoapod, rust-stable-testing, rust-fuzzing]
needs:
- java-docker
- java-reproducibility
- java-extra-bridging-checks
- android-emulator-tests
- aarch64
- node-docker
- node-reproducibility
- node-windows-arm64
- swift-cocoapod
- rust-stable-testing
- rust-fuzzing
if: ${{ failure() && github.event_name == 'schedule' }}
permissions:
@ -549,7 +572,7 @@ jobs:
contents: write
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
github.rest.repos.createCommitComment({

View File

@ -1,4 +1,4 @@
name: 'Close stale issues and PRs'
name: "[auto] Close stale issues and PRs"
on:
schedule:
- cron: '15 12 * * *' # 7:15 EST, early in a workday

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
/.idea
.idea
*.iml
/target
/swift/**/.build

2
.nvmrc
View File

@ -1 +1 @@
20.11.1
24.13.0

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

View File

@ -9,7 +9,7 @@ These should usually be prioritized in that order, but adjust the trade-off as n
# General
- **The bridging layer is not API.** As noted in the [readme](README.md), the primary purpose of this library is to provide good Java, Swift, and TypeScript APIs. We also try to make the non-bridge crates have a nice API, both for our own maintainence, testing, and internal use; and for external users who want to use or fork our crate. However, the Rust APIs in rust/bridge/ and the raw C symbols / JNI entry points / Node module we build are not considered public-facing at all. Use that to keep everything else nice!
- **The bridging layer is not API.** As noted in the [readme](README.md), the primary purpose of this library is to provide good Java, Swift, and TypeScript APIs. We also try to make the non-bridge crates have a nice API, both for our own maintenance, testing, and internal use; and for external users who want to use or fork our crate. However, the Rust APIs in rust/bridge/ and the raw C symbols / JNI entry points / Node module we build are not considered public-facing at all. Use that to keep everything else nice!
(Not that you should be sloppy in the bridging layer. Maintainability is still a priority!)
@ -45,7 +45,7 @@ These should usually be prioritized in that order, but adjust the trade-off as n
- **Prefer `expect()` to `unwrap()`.** As noted, we don't have a no-panics policy, but `expect()` forces you to write down why you believe something should *never* happen except for programmer errors. In particular, untrusted input that fails to validate should *not* panic.
(Yes, there's a Clippy lint for this, but we also have a lot of code that predates this guideline.)
As an exception, it's okay to use `unwrap()` in tests, though `expect()` is still preferred if it's for the thing you're actively testing.
- You don't have to write doc comments on everything, but **if you do write a comment, make it a doc comment**, because they show up more nicely in IDEs.
@ -53,7 +53,7 @@ These should usually be prioritized in that order, but adjust the trade-off as n
- Crate-level Cargo.tomls don't usually inherit the workspace `rust-version`, because many crates are relatively stable and may continue working for external folks using earlier versions of Rust even though we no longer test for them; picking up the top-level MSRV update would therefore be unnecessarily breaking. Instead, they have a `rust-version` that indicates a known minimum at some point in the past; it may be too low, but it will never be overly high. The exceptions are the `bridge` crates, which are not intended to be used for anything but the app language libraries.
- **We do not have a changelog file**; we rely on [GitHub displaying all our releases](https://github.com/signalapp/libsignal/releases).
- **We do not have a changelog file**; we rely on [GitHub displaying all our releases](https://github.com/signalapp/libsignal/releases). Unreleased changes are collected in [RELEASE_NOTES.md][], which is reset after each release.
- **Avoid `cargo add`**, or fix up the Cargo.toml afterwards. Some of our dependency lists are organized and `cargo add` doesn't respect that.
@ -65,6 +65,10 @@ These should usually be prioritized in that order, but adjust the trade-off as n
These are automatically detected on x86_64, but will require an opt-in for aarch64 until we can update to `aes 0.9` or newer (not out yet at the time of this writing). All our app library build scripts set this themselves, but doing a manual `cargo build --release` will not.
- Our bridging logic uses code generation tools for the app-language interface files (C header for Swift, wrapper APIs for Java/Kotlin and TypeScript). These tools, or the macros used with them, depend on how types are written in `#[bridge_fn]` and other bridged APIs. Therefore, **use qualified names for non-std, non-libsignal types** in bridged signatures, so that they can be matched specifically and without ambiguity.
(There is one exception: `uuid::Uuid` has been `Uuid` for a long time, and is sufficiently unique to justify leaving it that way.)
## Async
@ -81,7 +85,7 @@ These should usually be prioritized in that order, but adjust the trade-off as n
# Java
- Many of our APIs are shared between Android and Server, and we also run the client tests on desktop machines, so **stick to Java 8** unless you've verified that something newer is available on Android (back to our earliest supported version, API level 21, at the time of this writing), and don't use Android-specific APIs unless you're actually in Android-specific code. (This *should* be checked in CI but things have slipped through before, and it'll save you time to know whether you're allowed to use something.)
- Many of our APIs are shared between Android and Server, and we also run the client tests on desktop machines, so **stick to Java 8** unless you've verified that something newer is available on Android (back to our earliest supported version, API level 23, at the time of this update), and don't use Android-specific APIs unless you're actually in Android-specific code. (This *should* be checked in CI but things have slipped through before, and it'll save you time to know whether you're allowed to use something.)
- **Put server-specific APIs in the server/ folder if they're not needed to test client features**, so they don't add code size for Android.

2287
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
members = [
"rust/attest",
"rust/crypto",
"rust/debug",
"rust/device-transfer",
"rust/keytrans",
"rust/media",
@ -21,6 +22,7 @@ members = [
"rust/bridge/jni/impl",
"rust/bridge/jni/testing",
"rust/bridge/node",
"rust/bridge/node/native_ts",
]
default-members = [
"rust/crypto",
@ -37,10 +39,10 @@ default-members = [
resolver = "2" # so that our dev-dependency features don't leak into products
[workspace.package]
version = "0.85.1"
version = "0.94.1"
authors = ["Signal Messenger LLC"]
license = "AGPL-3.0-only"
rust-version = "1.85"
rust-version = "1.88"
[workspace.lints.clippy]
# Prefer TryFrom between integers unless truncation is desired.
@ -60,11 +62,13 @@ device-transfer = { path = "rust/device-transfer" }
libsignal-account-keys = { path = "rust/account-keys" }
libsignal-cli-utils = { path = "rust/cli-utils" }
libsignal-core = { path = "rust/core" }
libsignal-debug = { path = "rust/debug" }
libsignal-keytrans = { path = "rust/keytrans" }
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" }
@ -84,17 +88,18 @@ signal-neon-futures = { path = "rust/bridge/node/futures" }
# Our forks of some dependencies, accessible as xxx_signal so that usages of them are obvious in source code. Crates
# that want to use the real things can depend on those directly.
boring-signal = { git = "https://github.com/signalapp/boring", tag = "signal-v4.18.0", package = "boring", default-features = false }
boring-signal = { git = "https://github.com/signalapp/boring", tag = "signal-v5.0.2", package = "boring", default-features = false }
curve25519-dalek-signal = { git = 'https://github.com/signalapp/curve25519-dalek', package = "curve25519-dalek", tag = 'signal-curve25519-4.1.3' }
spqr = { git = "https://github.com/signalapp/SparsePostQuantumRatchet.git", tag = "v1.2.0" }
tokio-boring-signal = { git = "https://github.com/signalapp/boring", package = "tokio-boring", tag = "signal-v4.18.0" }
spqr = { git = "https://github.com/signalapp/SparsePostQuantumRatchet.git", tag = "v1.5.1" }
tokio-boring-signal = { git = "https://github.com/signalapp/boring", tag = "signal-v5.0.2", package = "tokio-boring" }
aes = "0.8.3"
aes-gcm-siv = "0.11.1"
anyhow = "1.0.97"
arbitrary = "1.4.2"
argon2 = "0.5.0"
arrayvec = "0.7.4"
asn1 = "0.21.0"
asn1 = "0.23.0"
assert_cmd = "2.0.13"
assert_matches = "1.5"
async-compression = "0.4.5"
@ -106,21 +111,21 @@ bincode = "1.3.2"
bitflags = "2.9"
bitstream-io = "1.10.0"
blake2 = "0.10.6"
boring = { version = "4.6.0", default-features = false }
boring-sys = { version = "4.6.0", default-features = false }
bytes = "1.9.0"
boring = { version = "5.0", default-features = false }
boring-sys = { version = "5.0", default-features = false }
bytes = "1.11.1"
cbc = "0.1.2"
cfg-if = "1.0.0"
chacha20poly1305 = "0.10.1"
chrono = "0.4.23"
chrono = "0.4.42"
clap = "4.4.11"
clap-stdin = "0.6.0"
const-str = "0.6.2"
clap-stdin = "0.8.0"
const-str = "1.0"
criterion = "0.5"
ctr = "0.9.2"
curve25519-dalek = "4.1.3"
data-encoding-macro = "0.1.18"
derive-where = "1.2.7"
derive-where = "1.6.1"
derive_more = "2.0.0"
dir-test = "0.4.1"
displaydoc = "0.2.5"
@ -128,28 +133,27 @@ ed25519-dalek = "2.1.0"
either = "1.13.0"
env_logger = "0.11.7"
flate2 = { version = "1.1.1", default-features = false }
foreign-types = "0.5"
futures = "0.3"
futures-util = "0.3"
ghash = "0.5.0"
heck = "0.5"
hex = "0.4.3"
hickory-proto = "0.24.1"
hickory-proto = "0.26.1"
hkdf = "0.12"
hmac = "0.12.0"
hpke-rs = "0.3.0"
hpke-rs-crypto = "0.3.0"
hpke-rs = "0.6.1"
hpke-rs-crypto = "0.6.1"
http = "1.3.0"
http-body = "1.0.1"
http-body-util = "0.1.3"
hyper = "1.7"
hyper-util = "0.1.17"
indexmap = "2.1.0"
indexmap = "2.7.0"
intmap = "3.1.2"
itertools = "0.14.0"
jni = "0.21"
json5 = "0.4.1"
libc = "0.2.175"
libcrux-ml-kem = { version = "0.0.2", default-features = false }
libc = "0.2.186"
libcrux-ml-kem = { version = "0.0.8", default-features = false }
linkme = "0.3.33"
log = "0.4.21"
log-panics = "2.1.0"
@ -158,34 +162,41 @@ 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"
once_cell = "1.19.0"
once_cell = "1.20.0"
partial-default = "0.1.0"
paste = "1.0.15"
pbjson = "0.9.0"
pbjson-build = "0.9.0"
pbjson-types = "0.9.0"
pin-project = "1.1.5"
pretty_assertions = "1.4.0"
proc-macro2 = "1.0.93"
proptest = "1.7"
proptest-arbitrary-interop = "0.1.0"
proptest-state-machine = "0.4"
prost = "0.13.5"
prost-build = "0.13.5"
prost = "0.14"
prost-build = "0.14"
prost-types = "0.14"
protobuf = "3.7.2"
protobuf-codegen = "3.7.2"
protobuf-json-mapping = "3.7.2"
quote = "1.0.38"
rand = "0.9"
quote = "1.0.40"
rand = "0.9.4"
rand_chacha = "0.9"
rand_core = "0.9"
rangemap = "1.5.1"
rayon = "1.8.0"
rcgen = "0.13.0"
rcgen = "0.14.0"
ref-cast = "1.0.25"
rustls = { version = "0.23.25", default-features = false }
rustls-platform-verifier = "0.5.1"
scopeguard = "1.0"
serde = "1.0.203"
serde_json = "1.0.45"
serde_json5 = "0.2.1"
serde_with = "3.1.0"
sha1 = "0.10"
sha2 = "0.10"
@ -201,31 +212,44 @@ 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.27.0"
tokio-util = "0.7.11"
tonic = "0.13.1"
tonic-build = "0.13.1"
tower = "0.5.2"
tungstenite = "0.27.0"
tokio-tungstenite = "0.28.0"
tokio-util = "0.7.18"
tonic = { version = "0.14", default-features = false }
tonic-prost = "0.14"
tonic-prost-build = { version = "0.14", default-features = false }
tower-service = "0.3.3"
tungstenite = "0.28.0"
unicode-segmentation = "1.12.0"
url = "2.4.1"
url = "2.5.4"
uuid = "1.5"
visibility = "0.1.1"
warp = "0.4.2"
webpsan = { version = "0.5.3", default-features = false }
x25519-dalek = "2.0.0"
zerocopy = "0.8.24"
zerocopy = "0.8.33"
zeroize = "1.8.2"
[patch.crates-io]
# When building libsignal, just use our forks so we don't end up with two different versions of the libraries.
boring = { git = 'https://github.com/signalapp/boring', tag = 'signal-v4.18.0' }
boring-sys = { git = 'https://github.com/signalapp/boring', tag = 'signal-v4.18.0' }
boring = { git = 'https://github.com/signalapp/boring', tag = "signal-v5.0.2" }
boring-sys = { git = 'https://github.com/signalapp/boring', tag = "signal-v5.0.2" }
curve25519-dalek = { git = 'https://github.com/signalapp/curve25519-dalek', tag = 'signal-curve25519-4.1.3' }
tungstenite = { git = 'https://github.com/signalapp/tungstenite-rs', tag = 'signal-v0.27.0' }
[profile.dev.package.argon2]
opt-level = 2 # libsignal-account-keys unit tests are too slow with an unoptimized argon2
[profile.release]
overflow-checks = true
[profile.release.package.curve25519-dalek]
overflow-checks = false
[profile.release.package.sha2]
overflow-checks = false
[profile.release.package.hmac]
overflow-checks = false

View File

@ -5,7 +5,7 @@
Pod::Spec.new do |s|
s.name = 'LibSignalClient'
s.version = '0.85.1'
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'
@ -15,7 +15,6 @@ Pod::Spec.new do |s|
s.swift_version = '5'
s.platform = :ios, '15.0'
s.libraries = ['z']
s.source_files = ['swift/Sources/**/*.swift', 'swift/Sources/**/*.m']
s.preserve_paths = [
@ -58,14 +57,20 @@ Pod::Spec.new do |s|
'ARCHS[sdk=iphonesimulator*]' => 'x86_64 arm64',
'ARCHS[sdk=iphoneos*]' => 'arm64',
}
user_target_xcconfig = {}
if ENV['LIBSIGNAL_TESTING_ONLY_ACTIVE_ARCH']
pod_target_xcconfig['ONLY_ACTIVE_ARCH'] = 'YES'
s.user_target_xcconfig = { 'ONLY_ACTIVE_ARCH' => 'YES' }
user_target_xcconfig['ONLY_ACTIVE_ARCH'] = 'YES'
end
# This pod does not currently support explicit modules, but clients should specify that explicitly.
# `pod lib lint` doesn't provide that opportunity though.
if ENV['LIBSIGNAL_TESTING_DISABLE_EXPLICIT_MODULES']
user_target_xcconfig['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO'
end
s.pod_target_xcconfig = pod_target_xcconfig
s.user_target_xcconfig = user_target_xcconfig
s.script_phases = [
{ name: 'Download libsignal-ffi if not in cache',

View File

@ -43,15 +43,15 @@ increases to the minimum supported tools versions.
### Toolchain Installation
To build anything in this repository you must have [Rust](https://rust-lang.org) installed,
as well as Clang, libclang, [CMake](https://cmake.org), Make, protoc, and git.
To build anything in this repository you must have [Rust](https://rust-lang.org) installed, as well
as recent versions of Clang, libclang, [CMake](https://cmake.org), Make, protoc, Python (3.9+), and git.
#### Linux/Debian
On a Debian-like system, you can get these extra dependencies through `apt`:
```shell
$ apt-get install clang libclang-dev cmake make protobuf-compiler git
$ apt-get install clang libclang-dev cmake make protobuf-compiler libprotobuf-dev python3 git
```
#### macOS
@ -88,12 +88,13 @@ You should always install any Rust tools you need that may affect the build from
package manager (e.g. `apt` or `brew`). Package managers sometimes contain outdated versions of these tools that can break
the build with incompatibility issues (especially cbindgen).
To install the main Rust extra dependencies matching the versions we use, you can run the following commands:
To install the main Rust extra dependencies matching the versions we use, you can run the following commands:
```shell
$ cargo +stable install cbindgen cargo-fuzz
$ cargo +stable install --version "$(cat ../acknowledgments/cargo-about-version)" --locked cargo-about
$ cargo +stable install --version "$(cat ../.taplo-cli-version)" --locked taplo-cli
$ cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
$ cargo +stable install --version "$(cat acknowledgments/cargo-about-version)" --locked cargo-about
$ cargo +stable install --version "$(cat .taplo-cli-version)" --locked taplo-cli
$ cargo +stable install cargo-fuzz
```
## Java/Android
@ -144,22 +145,34 @@ $ make
When exposing new APIs to Java, you will need to run `rust/bridge/jni/bin/gen_java_decl.py` in
addition to rebuilding. This requires installing the `cbindgen` Rust tool, as detailed above.
### Maven Central
### Use as a library
Signal publishes Java packages on [Maven Central](https://central.sonatype.org) for its own use,
under the names org.signal:libsignal-server, org.signal:libsignal-client, and
org.signal:libsignal-android. libsignal-client and libsignal-server contain native libraries for
Debian-flavored x86_64 Linux as well as Windows (x86_64) and macOS (x86_64 and arm64).
libsignal-android contains native libraries for armeabi-v7a, arm64-v8a, x86, and x86_64 Android.
Signal publishes Java packages for its own use, under the names org.signal:libsignal-server,
org.signal:libsignal-client, and org.signal:libsignal-android. libsignal-client and libsignal-server
contain native libraries for Debian-flavored x86_64 Linux as well as Windows (x86_64) and macOS
(x86_64 and arm64). libsignal-android contains native libraries for armeabi-v7a, arm64-v8a, x86, and
x86_64 Android. These are located in a Maven repository at
https://build-artifacts.signal.org/libraries/maven/; for use from Gradle, add the following to your
`repositories` block:
```
maven {
name = "SignalBuildArtifacts"
// The "uri()" part is only necessary for Kotlin Gradle; Groovy Gradle accepts a bare string here.
url = uri("https://build-artifacts.signal.org/libraries/maven/")
}
```
Older builds were published to [Maven Central](https://central.sonatype.org) instead.
When building for Android you need *both* libsignal-android and libsignal-client, but the Windows
and macOS libraries in libsignal-client won't automatically be excluded from your final app. You can
explicitly exclude them using `packagingOptions`:
explicitly exclude them using `packaging`:
```
android {
// ...
packagingOptions {
packaging {
resources {
excludes += setOf("libsignal_jni*.dylib", "signal_jni*.dll")
}
@ -171,6 +184,12 @@ android {
You can additionally exclude `libsignal_jni_testing.so` if you do not plan to use any of the APIs
intended for client testing.
### Testing a local build with Signal-Android
The Signal-Android gradle.properties file has a commented-out line to include libsignal as part of the build. Uncomment that and adjust the path; optionally, you can restrict the architectures you want to build for by adding `androidArchs=aarch64` to *libsignal's* gradle.properties. (The set of recognized architectures is in java/build_jni.sh.) If you're using an IDE, you'll need to re-import the Gradle structure at this point. When you're done, revert the changes to the Android app's gradle.properties and re-import once more.
Note that this does not import the *Rust* parts of the project into the IDE. Doing that in a multi-language IDE like IDEA is possible, but finicky; as of 2025 the most reliable way to do it is to open the Android project first, add the libsignal repo root directory as a Rust project second (only including the top-level directory), and only then make the changes to gradle.properties.
## Swift
@ -195,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
@ -207,6 +226,10 @@ libraries for Windows, macOS, and Debian-flavored Linux. Both x64 and arm64 buil
all three platforms, but the arm64 builds for Windows and Linux are considered experimental, since
there are no official builds of Signal for those architectures.
### Testing a local build with Signal-Desktop
After running all the build commands above, adjust the `@signalapp/libsignal-client` dependency in the Desktop app's package.json to "link:path/to/libsignal/node" and run `pnpm install`. When you're done, revert the changes to package.json and run `pnpm install` again.
# Contributions
@ -253,6 +276,6 @@ Administration Regulations, Section 740.13) for both object code and source code
## License
Copyright 2020-2024 Signal Messenger, LLC
Copyright 2020-2026 Signal Messenger, LLC
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html

View File

@ -54,19 +54,17 @@ git push <remote> HEAD:main <release tag, e.g. v0.x.y>
## 3. Submit to package repositories as needed
### Android and Server: Sonatype
### Android and Server: Maven
In the signalapp/libsignal repository on GitHub, run the "Upload Java libraries to Sonatype" action on the tag you just made. Then go to [Maven Central][] and wait for the build to show up (it can take up to an hour).
[Maven Central]: https://central.sonatype.com/artifact/org.signal/libsignal-client/versions
In the signalapp/libsignal repository on GitHub, run the "Release - Java" action on the tag you just made.
### Node: NPM
In the signalapp/libsignal repository on GitHub, run the "Publish to NPM" action on the tag you just made. Leave the "NPM Tag" as "latest".
In the signalapp/libsignal repository on GitHub, run the "Release - NPM" action on the tag you just made. Leave the "NPM Tag" as "latest".
### iOS: Build Artifacts
In the signalapp/libsignal repository on GitHub, run the "Build iOS Artifacts" action on the tag you just made. Share the resulting checksum with whoever will update the iOS app repository.
In the signalapp/libsignal repository on GitHub, run the "Release - iOS" action on the tag you just made. Share the resulting checksum with whoever will update the iOS app repository.
## Appendix: Release Standards and Information

View File

@ -1,7 +1,5 @@
v0.85.1
v0.94.1
- Backups / SVRB - add support for multiple SVRB backends when new enclaves need to roll out.
- 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.
- Typed APIs: `UnauthUsernamesService.lookUpUsernameLink` has been added.
- Backup validator: count grapheme clusters instead of characters in poll strings.
- keytrans: Add reset account data field functionality for all platforms.

View File

@ -42,7 +42,7 @@ Rust allows running tests with cross-compiled targets, but normally that only wo
ANDROID_NDK_HOME=path/to/ndk
CARGO_PROFILE_TEST_STRIP=debuginfo # make the "push" step take less time
CARGO_PROFILE_BENCH_STRIP=debuginfo # same for benchmarks
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=path/to/ndk/toolchains/llvm/prebuilt/YOUR_HOST_HERE/bin/aarch64-linux-android21-clang
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=path/to/ndk/toolchains/llvm/prebuilt/YOUR_HOST_HERE/bin/aarch64-linux-android23-clang
CARGO_TARGET_AARCH64_LINUX_ANDROID_RUNNER=bin/adb-run-test # in the repo root
```

View File

@ -8,7 +8,6 @@ accepted = [
"ISC",
"MPL-2.0",
"AGPL-3.0-only",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
]
@ -29,9 +28,33 @@ workarounds = [
"chrono",
"prost",
"ring",
"tonic",
]
# The async-compression crates are embedded in a larger repo
[async-compression.clarify]
license = "MIT"
[[async-compression.clarify.git]]
path = "LICENSE-MIT"
checksum = "88d1e3160df48926ad3310a8ec5699b502889565908f1be7e77cd21282c7a709"
[compression-codecs.clarify]
license = "MIT"
[[compression-codecs.clarify.git]]
path = "LICENSE-MIT"
checksum = "88d1e3160df48926ad3310a8ec5699b502889565908f1be7e77cd21282c7a709"
[compression-core.clarify]
license = "MIT"
[[compression-core.clarify.git]]
path = "LICENSE-MIT"
checksum = "88d1e3160df48926ad3310a8ec5699b502889565908f1be7e77cd21282c7a709"
# Boring's main license isn't at the root of the repo
[boring.clarify]
license = "Apache-2.0"
@ -46,7 +69,7 @@ checksum = "48e488ce333f8a1e86a68b2a1df454464037f1ff580b5bff926053c56dbadc2d"
# and the similar configuration for 'ring' in https://github.com/EmbarkStudios/cargo-about/blob/3bcd3380f606fd468b2836e04cdcf7997d1f3ff8/src/licenses/workarounds/ring.rs
[boring-sys.clarify]
license = "MIT AND ISC AND OpenSSL"
license = "MIT AND Apache-2.0"
[[boring-sys.clarify.files]]
# The MIT license of the Rust code
@ -55,28 +78,10 @@ license = "MIT"
checksum = "ad2e7bdef7c00b92eaf4f657a472c7d3f8b36aac3cdc270e65bb0c287eec0d4e"
[[boring-sys.clarify.files]]
# The original OpenSSL license
# The Apache 2.0 license of BoringSSL
path = "deps/boringssl/LICENSE"
license = "OpenSSL"
start = "/* ===================================================================="
end = "*/"
checksum = "53552a9b197cd0db29bd085d81253e67097eedd713706e8cd2a3cc6c29850ceb"
[[boring-sys.clarify.files]]
# The ISC license of the Google-written BoringSSL code
path = "deps/boringssl/LICENSE"
license = "ISC"
start = "/* Copyright (c) 2015, Google Inc."
end = "* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */"
checksum = "19c779f8bbc141fa15c14e0a15aacaee2da917f7043af883c90cbef3cd6f4847"
[[boring-sys.clarify.files]]
# The MIT license of the BoringSSL code in third_party/fiat
path = "deps/boringssl/LICENSE"
license = "MIT"
start = "Copyright (c) 2015-2016 the fiat-crypto authors"
end = "SOFTWARE."
checksum = "7d5e1fb4bbd5e89a687f94c3d3826db50e26bd6f4ade136a025dc2080c5bdc85"
license = "Apache-2.0"
checksum = "827c8d8fc207c2392794eef9e00fe246f9f61fdcc132556c275be3dd8c3cd97f"
# Newer versions of convert_case have a LICENSE file, we'll use that one
@ -194,6 +199,13 @@ license = "Apache-2.0"
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-secrets.clarify]
license = "Apache-2.0"
[[libcrux-secrets.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-sha2.clarify]
license = "Apache-2.0"
@ -302,6 +314,22 @@ path = "LICENSE"
checksum = "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594"
# The tonic-prost crates are embedded in a larger repo
[tonic-prost.clarify]
license = "MIT"
[[tonic-prost.clarify.git]]
path = "LICENSE"
checksum = "e24a56698aa6feaf3a02272b3624f9dc255d982970c5ed97ac4525a95056a5b3"
[tonic-prost-build.clarify]
license = "MIT"
[[tonic-prost-build.clarify.git]]
path = "LICENSE"
checksum = "e24a56698aa6feaf3a02272b3624f9dc255d982970c5ed97ac4525a95056a5b3"
# webpsan is embedded in a larger repo
[webpsan.clarify]
license = "MIT"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

20
bin/benchmark-criterion Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Workaround for https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
# Invoke this like: ./bin/benchmark-criterion -p foo -- --save-baseline my_baseline
# Any arguments after the "--" will _only_ be passed to criterion binaries.
set -euo pipefail
SCRIPT_DIR=$(dirname "$0")
cd "${SCRIPT_DIR}"/..
declare -a CARGO_ARGS
while [[ "${1:-}" != "--" ]] && (! [[ -z "${1:-}" ]]); do
CARGO_ARGS+=("$1")
shift 1
done
if [[ "${1:-}" == "--" ]]; then
shift 1
fi
export LIBSIGNAL_BENCHMARK_ARGS="$@"
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="$PWD/bin/benchmark-criterion-helper.sh"
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER="$PWD/bin/benchmark-criterion-helper.sh"
export CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER="$PWD/bin/benchmark-criterion-helper.sh"
exec cargo bench "${CARGO_ARGS[@]}"

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
# Don't invoke this directly. See ./benchmark-criterion
# This script is intended to be invoked as a cargo runner.
# Add $LIBSIGNAL_BENCHMARK_ARGS to the arguments if the benchmark is criterion.
if "$@" --help 2>&1 | grep criterion > /dev/null; then
# We intentionally want to expand the args here
# shellcheck disable=SC2086
exec "$@" $LIBSIGNAL_BENCHMARK_ARGS
else
exec "$@"
fi

View File

@ -36,14 +36,14 @@ def build_argument_parser() -> argparse.ArgumentParser:
def download_if_needed(archive_file: str, url: str, checksum: str) -> BinaryIO:
try:
f = open(archive_file, 'rb')
fr = open(archive_file, 'rb')
digest = hashlib.sha256()
chunk = f.read1()
chunk = fr.read1()
while chunk:
digest.update(chunk)
chunk = f.read1()
chunk = fr.read1()
if digest.hexdigest() == checksum.lower():
return f
return fr
print("existing file '{}' has non-matching checksum {}; re-downloading...".format(archive_file, digest.hexdigest()), file=sys.stderr)
except FileNotFoundError:
pass
@ -52,15 +52,15 @@ def download_if_needed(archive_file: str, url: str, checksum: str) -> BinaryIO:
try:
with urllib.request.urlopen(url) as response:
digest = hashlib.sha256()
f = open(UNVERIFIED_DOWNLOAD_NAME, 'w+b')
fw = open(UNVERIFIED_DOWNLOAD_NAME, 'w+b')
chunk = response.read1()
while chunk:
digest.update(chunk)
f.write(chunk)
fw.write(chunk)
chunk = response.read1()
assert digest.hexdigest() == checksum.lower(), 'expected {}, actual {}'.format(checksum.lower(), digest.hexdigest())
os.replace(UNVERIFIED_DOWNLOAD_NAME, archive_file)
return f
return fw
except (urllib.error.HTTPError, urllib.error.URLError) as e:
if isinstance(e.reason, ssl.SSLCertVerificationError):
# See:

View File

@ -2,7 +2,7 @@
#set -ex
cat << EOF | brew bundle install --file=-
brew bundle install --file=- << EOF
brew "awscli"
brew "cmake"
brew "cocoapods"
@ -12,6 +12,7 @@ brew "gh"
brew "git"
brew "jq"
brew "just"
brew "pipx"
brew "protobuf"
brew "python"
brew "rocksdb"
@ -20,7 +21,17 @@ 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<2.0"
"$(brew --prefix pipx)/bin/pipx" install flake8
"$(brew --prefix pipx)/bin/pipx" inject flake8 \
flake8-comprehensions \
flake8-deprecated \
flake8-import-order \
flake8-quotes

View File

@ -30,6 +30,7 @@ import re
import subprocess
import sys
import time
import traceback
from pathlib import Path
from shutil import which
@ -45,6 +46,18 @@ class ReleaseFailedException(Exception):
# execute the rest while performing a rollback.
on_failure_rollback_commands: list[list[str]] = []
# The following ids can be obtained by running:
# gh workflow list
# or, more programmatically friendly,
# gh workflow list --json id,name
BUILD_AND_TEST_WORKFLOW_ID = 6587503
SLOW_TEST_WORKFLOW_ID = 30989402
RELEASE_WORKFLOW_IDS = [
10143338, # Node
15104239, # Android
46287777, # iOS
]
def main() -> None:
parser = argparse.ArgumentParser(
@ -87,7 +100,7 @@ def main() -> None:
print('User interrupted execution! Aborting...')
exit_code = 1
except Exception as ex:
print(f'Unexpected error: {ex}')
traceback.print_exception(None, value=ex, tb=ex.__traceback__)
exit_code = 1
if exit_code != 0:
@ -101,11 +114,27 @@ def main() -> None:
sys.exit(exit_code)
def get_workflow_name_mapping(repo_name: str) -> dict[int, str]:
"""Gets a mapping of workflow ids to their names from github."""
list_workflows_cmd = [
'gh', 'workflow', 'list',
'--repo', f'signalapp/{repo_name}',
'--json', 'name,id'
]
raw_json = run_command(list_workflows_cmd)
data = json.loads(raw_json)
return {d['id']: d['name'] for d in data}
def prepare_release(*, skip_main_check: bool = False, skip_tests_pass_check: bool = False, skip_worktree_clean_check: bool = False, dry_run: bool = False) -> None:
setup_and_check_env(skip_main_check, skip_worktree_clean_check)
REPO_NAME = get_repo_name()
RELEASE_NOTES_FILE_PATH = Path('RELEASE_NOTES.md')
# Obtain the workflow ids once
workflows = get_workflow_name_mapping(REPO_NAME)
# Get the commit sha of the commit we intend to mark as the release.
head_sha = run_command(['git', 'rev-parse', 'HEAD']).strip()
short_sha = head_sha[:9]
@ -117,8 +146,8 @@ def prepare_release(*, skip_main_check: bool = False, skip_tests_pass_check: boo
# If needed, you can run the Slow Tests manually under the repository Actions tab on GitHub.
# You should run the Slow Tests before running this script.
if not skip_tests_pass_check:
build_and_test_run_id = check_workflow_success(REPO_NAME, 'Build and Test', head_sha)
slow_test_run_id = check_workflow_success(REPO_NAME, 'Slow Tests', head_sha)
build_and_test_run_id = check_workflow_success(REPO_NAME, workflows[BUILD_AND_TEST_WORKFLOW_ID], head_sha)
slow_test_run_id = check_workflow_success(REPO_NAME, workflows[SLOW_TEST_WORKFLOW_ID], head_sha)
print('Found GitHub Actions runs! They look good, but please double check manually as well.')
print(f'Build and Test: https://github.com/signalapp/{REPO_NAME}/actions/runs/{build_and_test_run_id}')
@ -221,9 +250,14 @@ def prepare_release(*, skip_main_check: bool = False, skip_tests_pass_check: boo
print('Next steps:')
print('1) Verify the GitHub Actions runs above passed.')
print('2) If they passed, push to the proper remote(s), e.g.:')
print(f' git push {upstream} HEAD~1:main {head_release_version} && git push {origin} HEAD:main {head_release_version}')
print(f'\tgit push {upstream} HEAD~1:main {head_release_version} && git push {origin} HEAD:main {head_release_version}')
print('3) To review the reset commit, you can run:')
print(' git show')
print('\tgit show')
print('4) To run post-release actions, you can run:')
for id in RELEASE_WORKFLOW_IDS:
name = workflows[id]
raw_field = ' --raw-field dry_run=true' if dry_run else ''
print(f'\tgh workflow run "{name}" --repo signalapp/libsignal --ref {head_release_version}{raw_field}')
def setup_and_check_env(skip_main_check: bool = False, skip_worktree_clean_check: bool = False) -> None:
@ -388,6 +422,8 @@ def check_workflow_success(repo_name: str, workflow_name: str, head_sha: str) ->
if workflow_name == 'Slow Tests':
print('Note that Slow Tests do not run automatically.')
print(f'You must kick them off automatically at: https://github.com/signalapp/{repo_name}/actions/workflows/slow_tests.yml')
print('Or by running')
print(f'\tgh workflow run "{workflow_name}" --repo signalapp/{repo_name} --ref main')
print('If tests have actually passed, you can skip this check by re-running with --skip-ci-tests-pass-check')
raise ReleaseFailedException
@ -413,6 +449,8 @@ def check_workflow_success(repo_name: str, workflow_name: str, head_sha: str) ->
if status != 'completed' or conclusion != 'success':
print(f"Error: '{workflow_name}' did not succeed (status={status}, conclusion={conclusion}).")
print('Please ensure all CI checks have passed before releasing.')
print('You can watch the run using:')
print(f'\tgh run watch {selected_run_id}')
raise ReleaseFailedException
return selected_run_id

View File

@ -11,6 +11,24 @@ SCRIPT_DIR=$(dirname "$0")
cd "${SCRIPT_DIR}"/..
. bin/build_helpers.sh
CHECK=0
case "${1:-}" in
--check)
CHECK=1
shift
;;
esac
if [ "$#" -ne 0 ]; then
echo "usage: $0 [--check]" >&2
exit 2
fi
if [ "$CHECK" -eq 1 ]; then
OUTPUT_DIR=$(mktemp -d)
trap 'rm -rf "$OUTPUT_DIR"' EXIT
fi
echo "Checking cargo-about version"
VERSION=$(cargo about --version)
echo "Found $VERSION"
@ -32,6 +50,20 @@ generate() {
"$@"
}
generate_and_maybe_check() {
template="$1"
tracked_output="$2"
shift 2
if [ "$CHECK" -eq 1 ]; then
generated_output="${OUTPUT_DIR}/$(basename "$tracked_output")"
generate "$template" "$generated_output" "$@"
diff -u "$tracked_output" "$generated_output"
else
generate "$template" "$tracked_output" "$@"
fi
}
# List every target we ship, just in case some dependencies are platform-gated.
ANDROID_TARGETS=(
aarch64-linux-android
@ -50,12 +82,12 @@ DESKTOP_TARGETS=(
IOS_TARGETS=(aarch64-apple-ios)
# shellcheck disable=SC2068 # We want "--target" to end up as a separate argument.
generate acknowledgments/acknowledgments{.html.hbs,.html} ${DESKTOP_TARGETS[@]/#/--target } ${IOS_TARGETS[@]/#/--target } ${ANDROID_TARGETS[@]/#/--target } --workspace
generate_and_maybe_check acknowledgments/acknowledgments{.html.hbs,.html} ${DESKTOP_TARGETS[@]/#/--target } ${IOS_TARGETS[@]/#/--target } ${ANDROID_TARGETS[@]/#/--target } --workspace
# shellcheck disable=SC2068
generate acknowledgments/acknowledgments{.md.hbs,-android.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/Cargo.toml
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-android.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/Cargo.toml
# shellcheck disable=SC2068
generate acknowledgments/acknowledgments{.md.hbs,-android-testing.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/testing/Cargo.toml
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-android-testing.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/testing/Cargo.toml
# shellcheck disable=SC2068
generate acknowledgments/acknowledgments{.md.hbs,-desktop.md} ${DESKTOP_TARGETS[@]/#/--target } --manifest-path rust/bridge/node/Cargo.toml
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-desktop.md} ${DESKTOP_TARGETS[@]/#/--target } --manifest-path rust/bridge/node/Cargo.toml
# shellcheck disable=SC2068
generate acknowledgments/acknowledgments{.plist.hbs,-ios.plist} ${IOS_TARGETS[@]/#/--target } --manifest-path rust/bridge/ffi/Cargo.toml
generate_and_maybe_check acknowledgments/acknowledgments{.plist.hbs,-ios.plist} ${IOS_TARGETS[@]/#/--target } --manifest-path rust/bridge/ffi/Cargo.toml

View File

@ -12,22 +12,14 @@
# and then document them here.
#
# thiserror: minimal and highly inlinable, most of the code is synthesized at the use site
# rand_core, getrandom: waiting on snow to support rand_core 0.9
# hax-lib: highly inlinable, mostly annotated versions of simple operations
# libcrux-sha3, libcrux-intrinsics: v0.0.3 is referenced by hpke-rs, but only needed if you use X-Wing KEM
# rand_core, getrandom: waiting to update all the RustCrypto crates together
EXPECTED="
getrandom v0.2.16
getrandom v0.3.3
hax-lib v0.2.0
hax-lib v0.3.4
libcrux-intrinsics v0.0.2
libcrux-intrinsics v0.0.3
libcrux-sha3 v0.0.2
libcrux-sha3 v0.0.3
getrandom v0.3.4
rand_core v0.6.4
rand_core v0.9.3
thiserror v1.0.69
thiserror v2.0.16"
thiserror v2.0.17"
check_cargo_tree() {
# Only check the mobile targets, where we care most about code size.
@ -41,7 +33,16 @@ check_cargo_tree() {
"$@"
}
if [[ "$(check_cargo_tree --depth 0 | sort -u -V)" != "${EXPECTED}" ]]; then
ACTUAL="$(check_cargo_tree --depth 0 | sort -u -V)"
if [[ "${ACTUAL}" != "${EXPECTED}" ]]; then
cat <<EOF
----- EXPECTED -----
${EXPECTED}
------ ACTUAL ------
${ACTUAL}
EOF
check_cargo_tree
exit 1
fi

7
bin/without_building_boring.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
# This is expected to be the path to a BoringSSL *build* directory,
# but if we set it to the *source* directory it's enough for bindgen to run.
# And we can use a relative path because build scripts run from the package root).
export BORING_BSSL_PATH=deps/boringssl
command "$@"

1
java/.gitignore vendored
View File

@ -5,3 +5,4 @@ out
*.ipr
*.iws
*.iml
.kotlin

View File

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
#
FROM ubuntu:jammy-20230624@sha256:b060fffe8e1561c9c3e6dea6db487b900100fc26830b9ea2ec966c151ab4c020
FROM ubuntu:jammy-20260109@sha256:c7eb020043d8fc2ae0793fb35a37bff1cf33f156d4d4b12ccc7f3ef8706c38b1
COPY java/docker/ docker/
COPY java/docker/apt.conf java/docker/sources.list /etc/apt/
@ -61,17 +61,24 @@ RUN ./gradlew --version
# Rust setup...
COPY rust-toolchain rust-toolchain
ARG RUSTUP_SHA=ad1f8b5199b3b9e231472ed7aa08d2e5d1d539198a15c5b1e53c746aad81d27b
ARG RUSTUP_SHA=20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c
ENV PATH="/home/libsignal/.cargo/bin:${PATH}"
ADD --chown=libsignal --chmod=755 --checksum=sha256:${RUSTUP_SHA} \
https://static.rust-lang.org/rustup/archive/1.21.1/x86_64-unknown-linux-gnu/rustup-init /tmp/rustup-init
https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init /tmp/rustup-init
RUN /tmp/rustup-init -y --profile minimal --default-toolchain "$(cat rust-toolchain)" \
&& rm -rf /tmp/rustup-init
RUN rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android aarch64-unknown-linux-gnu
# Manually install a newer protoc
ADD --chown=libsignal https://github.com/protocolbuffers/protobuf/releases/download/v29.3/protoc-29.3-linux-x86_64.zip protoc.zip
RUN unzip protoc.zip -d proto && rm -f protoc.zip
ENV PATH="/home/libsignal/proto/bin:${PATH}"
# Install the full set of tools now that the long setup steps are done.
# Note that we temporarily hop back to root to do this.
USER root
@ -85,7 +92,6 @@ RUN apt-get install -y \
gpg-agent \
libclang-dev \
make \
protobuf-compiler \
python3
USER libsignal

View File

@ -11,11 +11,12 @@ default: java_build
DOCKER_IMAGE := libsignal-builder
DOCKER_TTY_FLAG := $$(test -t 0 && echo -it)
DOCKER_PLATFORM ?= linux/amd64
GRADLE_OPTIONS ?= --dependency-verification strict
CROSS_COMPILE_SERVER ?= -PcrossCompileServer
docker_image:
cd .. && $(DOCKER) build --build-arg UID=$$(id -u) --build-arg GID=$$(id -g) -t $(DOCKER_IMAGE) -f java/Dockerfile .
cd .. && $(DOCKER) build --platform=$(DOCKER_PLATFORM) --build-arg UID=$$(id -u) --build-arg GID=$$(id -g) -t $(DOCKER_IMAGE) -f java/Dockerfile .
java_build: DOCKER_EXTRA=$(shell [ -L build ] && P=$$(readlink build) && echo -v $$P/:$$P )
java_build: docker_image
@ -29,14 +30,13 @@ java_build: docker_image
publish_java: DOCKER_EXTRA = $(shell [ -L build ] && P=$$(readlink build) && echo -v $$P/:$$P )
publish_java: docker_image
$(DOCKER) run --rm --user $$(id -u):$$(id -g) \
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) \
-e ORG_GRADLE_PROJECT_sonatypeUsername \
-e ORG_GRADLE_PROJECT_sonatypePassword \
-e ORG_GRADLE_PROJECT_signingKeyId \
-e ORG_GRADLE_PROJECT_signingPassword \
-e ORG_GRADLE_PROJECT_signingKey \
-e CLOUDSDK_AUTH_ACCESS_TOKEN \
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) \
$(DOCKER_IMAGE) \
sh -c "cd src/java; ./gradlew $(GRADLE_OPTIONS) publish closeAndReleaseStagingRepositories $(CROSS_COMPILE_SERVER)"
sh -c "cd src/java; ./gradlew $(GRADLE_OPTIONS) publish $(CROSS_COMPILE_SERVER)"
# We could run these through Docker, but they would have the same result anyway.

View File

@ -1,5 +1,5 @@
plugins {
id 'com.android.library' version '8.10.1'
id 'com.android.library'
id 'androidx.benchmark' version '1.1.1'
}
@ -13,9 +13,9 @@ android {
compileSdk 34
defaultConfig {
minSdkVersion 21
minSdkVersion 23
targetSdkVersion 33
multiDexEnabled true
multiDexEnabled = true
testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner"
@ -27,15 +27,15 @@ android {
// }
}
testBuildType "release"
testBuildType = "release"
compileOptions {
coreLibraryDesugaringEnabled true
coreLibraryDesugaringEnabled = true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
namespace "org.signal.libsignal.benchmarks"
namespace = "org.signal.libsignal.benchmarks"
packagingOptions {
doNotStrip '**/*.so'

View File

@ -52,8 +52,10 @@ public class SealedSender {
InMemorySignalProtocolStore bobStore =
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), 0xBB);
SignalProtocolAddress bobAddress = new SignalProtocolAddress("+14152222222", 1);
SignalProtocolAddress aliceAddress =
new SignalProtocolAddress("9d0652a3-dcc3-4d11-975f-74d61598733f", 1);
initializeSessions(aliceStore, bobStore, bobAddress);
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
ECKeyPair trustRoot = ECKeyPair.generate();
SenderCertificate senderCertificate =
@ -66,10 +68,7 @@ public class SealedSender {
31337);
SealedSessionCipher aliceCipher =
new SealedSessionCipher(
aliceStore,
UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"),
"+14151111111",
1);
aliceStore, UUID.fromString(aliceAddress.getName()), "+14151111111", 1);
final BenchmarkState state = benchmarkRule.getState();
while (state.keepRunning()) {
@ -99,7 +98,7 @@ public class SealedSender {
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), i);
SignalProtocolAddress bobAddress =
new SignalProtocolAddress(UUID.randomUUID().toString(), i % 127 + 1);
filterExceptions(() -> initializeSessions(aliceStore, bobStore, bobAddress));
filterExceptions(() -> initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress));
recipients.add(bobAddress);
}
}
@ -187,7 +186,8 @@ public class SealedSender {
private static void initializeSessions(
InMemorySignalProtocolStore aliceStore,
InMemorySignalProtocolStore bobStore,
SignalProtocolAddress bobAddress)
SignalProtocolAddress bobAddress,
SignalProtocolAddress aliceAddress)
throws InvalidKeyException, UntrustedIdentityException {
ECKeyPair bobPreKey = ECKeyPair.generate();
IdentityKeyPair bobIdentityKey = bobStore.getIdentityKeyPair();
@ -207,7 +207,7 @@ public class SealedSender {
12,
bobKyberPreKey.getKeyPair().getPublicKey(),
bobKyberPreKey.getSignature());
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress);
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress, aliceAddress);
aliceSessionBuilder.process(bobBundle);
bobStore.storeSignedPreKey(2, bobSignedPreKey);

View File

@ -7,7 +7,9 @@ plugins {
id 'signing'
}
archivesBaseName = "libsignal-android"
base {
archivesName = "libsignal-android"
}
repositories {
google()
@ -16,15 +18,15 @@ repositories {
}
android {
namespace 'org.signal.libsignal'
namespace = 'org.signal.libsignal'
compileSdk 34
ndkVersion '28.0.13004108'
ndkVersion = '28.0.13004108'
defaultConfig {
minSdkVersion 21
minSdkVersion 23
targetSdkVersion 33
multiDexEnabled true
multiDexEnabled = true
testInstrumentationRunner "org.signal.libsignal.util.AndroidJUnitRunner"
// Automatically propagate matching environment variables into Java properties.
// See the custom AndroidJUnitRunner and TestEnvironment classes for more details.
@ -32,7 +34,7 @@ android {
}
compileOptions {
coreLibraryDesugaringEnabled true
coreLibraryDesugaringEnabled = true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
@ -69,14 +71,14 @@ kotlin {
}
task dokkaHtmlJar(type: Jar) {
dependsOn(dokkaHtml)
from(dokkaHtml)
dependsOn(dokkaGeneratePublicationHtml)
from(dokkaGeneratePublicationHtml)
archiveClassifier.set("dokka")
}
task dokkaJavadocJar(type: Jar) {
dependsOn(dokkaJavadoc)
from(dokkaJavadoc)
dependsOn(dokkaGeneratePublicationJavadoc)
from(dokkaGeneratePublicationJavadoc)
archiveClassifier.set("javadoc")
}
@ -120,6 +122,7 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'com.googlecode.json-simple:json-simple:1.1'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
androidTestImplementation 'org.jetbrains.kotlin:kotlin-test:2.1.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
api project(':client')
@ -135,16 +138,17 @@ String[] archsFromProperty(String prop) {
}
task makeJniLibraries(type:Exec) {
group 'Rust'
description 'Build the JNI libraries for Android'
group = 'Rust'
description = 'Build the JNI libraries for Android'
def archs = archsFromProperty('androidArchs') ?: ['android']
def debugLevelLogsFlag = project.hasProperty('debugLevelLogs') ? ['--debug-level-logs'] : []
def jniTypeTaggingFlag = project.hasProperty('jniTypeTagging') ? ['--jni-type-tagging'] : []
def jniCheckAnnotationsFlag = project.hasProperty('jniCheckAnnotations') ? ['--jni-check-annotations'] : []
def debugFlag = project.hasProperty('debugRust') ? ['--debug'] : []
def libsignalDebugFlag = project.hasProperty('libsignalDebug') ? ['--libsignal-debug'] : []
// Explicitly specify 'bash' for Windows compatibility.
commandLine 'bash', '../build_jni.sh', *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, *archs
commandLine 'bash', '../build_jni.sh', *libsignalDebugFlag, *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, *archs
environment 'ANDROID_NDK_HOME', android.ndkDirectory
}
@ -163,13 +167,13 @@ afterEvaluate {
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = archivesBaseName
artifactId = base.archivesName.get()
from components.release
artifact dokkaHtmlJar
artifact dokkaJavadocJar
pom {
name = archivesBaseName
name = base.archivesName.get()
packaging = 'aar'
description = 'Signal Protocol cryptography library for Android'
url = 'https://github.com/signalapp/libsignal'
@ -199,7 +203,7 @@ afterEvaluate {
setUpSigningKey(signing)
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask(":android:publish") }
required = { isReleaseBuild() && gradle.taskGraph.hasTask(":android:publish") }
sign publishing.publications.mavenJava
}
}

View File

@ -1,5 +1,5 @@
plugins {
id 'com.android.library' version '8.10.1'
id 'com.android.library'
}
repositories {
@ -12,15 +12,15 @@ android {
compileSdk 34
defaultConfig {
minSdkVersion 21
minSdkVersion 23
targetSdkVersion 33
multiDexEnabled true
multiDexEnabled = true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
coreLibraryDesugaringEnabled true
coreLibraryDesugaringEnabled = true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
@ -29,7 +29,7 @@ android {
jniLibs.excludes.add("**/libsignal_jni_testing.so")
}
namespace "org.signal.libsignal.packagingtest"
namespace = "org.signal.libsignal.packagingtest"
}
dependencies {

View File

@ -1,17 +0,0 @@
package org.signal.libsignal.util
import android.util.Base64
public class AndroidBase64 : org.signal.libsignal.util.Base64.Impl {
public override fun decode(encoded: ByteArray): ByteArray = Base64.decode(encoded, 0)
public override fun decodeUrl(encoded: ByteArray): ByteArray = Base64.decode(encoded, Base64.URL_SAFE)
public override fun encode(raw: ByteArray): String = Base64.encodeToString(raw, Base64.NO_WRAP)
public override fun encodeUrl(raw: ByteArray): String =
Base64.encodeToString(
raw,
Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE,
)
}

View File

@ -17,7 +17,8 @@ public class AndroidJUnitRunner extends androidx.test.runner.AndroidJUnitRunner
super.onCreate(bundle);
// Make sure libsignal logs get caught correctly.
SignalProtocolLoggerProvider.setProvider(new AndroidSignalProtocolLogger());
SignalProtocolLoggerProvider.setProvider(
new TestLoggerDecorator(new AndroidSignalProtocolLogger()));
SignalProtocolLoggerProvider.initializeLogging(SignalProtocolLogger.VERBOSE);
// Propagate any "environment variables" the test might need into System properties.

View File

@ -1,3 +1,4 @@
import org.gradle.api.publish.PublishingExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@ -5,14 +6,14 @@ plugins {
id "base"
id "signing"
id "com.diffplug.spotless" version "7.2.1"
id "io.github.gradle-nexus.publish-plugin" version "2.0.0"
id "org.jetbrains.kotlin.jvm" version "2.1.0" apply false
id "org.jetbrains.dokka" version "2.0.0" apply false
id "org.jetbrains.kotlin.jvm" version "2.2.20" apply false
id "org.jetbrains.dokka" version "2.1.0" apply false
id "org.jetbrains.dokka-javadoc" version "2.1.0" apply false
// These plugins need to be loaded together, so we must declare them up front.
id 'com.android.library' version "8.10.1" apply false
id 'org.jetbrains.kotlin.android' version "2.1.0" apply false
id 'com.android.library' version "8.13.2" apply false
id 'org.jetbrains.kotlin.android' version "2.2.20" apply false
}
repositories {
@ -22,7 +23,7 @@ repositories {
}
allprojects {
version = "0.85.1"
version = "0.94.1"
group = "org.signal"
tasks.withType(JavaCompile) {
@ -42,11 +43,12 @@ allprojects {
}
apply plugin: "org.jetbrains.dokka"
apply plugin: "org.jetbrains.dokka-javadoc"
}
task makeJniLibrariesDesktop(type:Exec) {
group 'Rust'
description 'Build the JNI libraries'
group = 'Rust'
description = 'Build the JNI libraries'
def debugLevelLogsFlag = project.hasProperty('debugLevelLogs') ? ['--debug-level-logs'] : []
def jniTypeTaggingFlag = project.hasProperty('jniTypeTagging') ? ['--jni-type-tagging'] : []
@ -57,8 +59,8 @@ task makeJniLibrariesDesktop(type:Exec) {
}
task makeJniLibrariesServer(type:Exec) {
group 'Rust'
description 'Build the JNI libraries'
group = 'Rust'
description = 'Build the JNI libraries'
def debugLevelLogsFlag = project.hasProperty('debugLevelLogs') ? ['--debug-level-logs'] : []
def jniTypeTaggingFlag = project.hasProperty('jniTypeTagging') ? ['--jni-type-tagging'] : []
@ -70,12 +72,12 @@ task makeJniLibrariesServer(type:Exec) {
}
task cargoClean(type:Exec) {
group 'Rust'
group = 'Rust'
commandLine 'cargo', 'clean'
}
task cleanJni(type: Delete) {
description 'Clean JNI libs'
description = 'Clean JNI libs'
delete fileTree('./android/src/main/jniLibs') {
include '**/*.so'
}
@ -104,15 +106,25 @@ ext.setUpSigningKey = { signingExt ->
}
}
nexusPublishing {
repositories {
sonatype {
username = project.findProperty('sonatypeUsername') ?: ""
password = project.findProperty('sonatypePassword') ?: ""
// This is the recommended configuration from the README for the plugin we use, gradle-nexus/publish-plugin.
// The URLs are from https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#configuration
nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/"))
snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/"))
subprojects { subproject ->
subproject.plugins.withId('maven-publish') {
subproject.extensions.configure(PublishingExtension) { publishing ->
publishing.repositories {
maven {
name = "SignalBuildArtifacts"
// We can't use Gradle's built-in GCS support with the way we authenticate
// GitHub Actions. Fortunately, GCS's REST APIs are basically just normal HTTP
// GET/PUT with an auth token, which is compatible with what Gradle will do.
url = subproject.uri("https://storage.googleapis.com/build-artifacts.signal.org/libraries/maven")
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = "Bearer ${System.getenv("CLOUDSDK_AUTH_ACCESS_TOKEN") ?: ""}"
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
}
}
}

View File

@ -17,7 +17,8 @@ DESKTOP_LIB_DIR=java/client/src/main/resources
SERVER_LIB_DIR=java/server/src/main/resources
# Fetch dependencies first, so we can use them in computing later options.
cargo fetch
# But allow this to fail in case we're offline.
cargo fetch || true
export CARGO_PROFILE_RELEASE_DEBUG=1 # Enable line tables
RUSTFLAGS="--cfg aes_armv8 ${RUSTFLAGS:-}" # Enable ARMv8 cryptography acceleration when available
@ -34,6 +35,10 @@ while [ "${1:-}" != "" ]; do
DEBUG_LEVEL_LOGS=1
shift
;;
--libsignal-debug )
LIBSIGNAL_DEBUG=1
shift
;;
--jni-type-tagging )
JNI_TYPE_TAGGING=1
shift
@ -64,6 +69,9 @@ fi
if [[ -n "${JNI_CHECK_ANNOTATIONS:-}" ]]; then
FEATURES+=("libsignal-bridge-types/jni-invoke-annotated")
fi
if [[ -n "${LIBSIGNAL_DEBUG:-}" ]]; then
FEATURES+=("libsignal-debug/enabled")
fi
# usage: check_for_debug_level_logs_if_needed lib_dir
check_for_debug_level_logs_if_needed () {
@ -82,6 +90,18 @@ check_for_debug_level_logs_if_needed () {
exit 2
fi
fi
# See libsignal-debug for the strings matched by this pattern.
if grep -q -- 'LIBSIGNAL-DEBUG IS ENABLED' "$1"/*; then
if [[ -z "${LIBSIGNAL_DEBUG:-}" ]]; then
echo 'error: libsignal-debug found in build that should not have it!' >&2
exit 2
fi
else
if [[ -n "${LIBSIGNAL_DEBUG:-}" ]]; then
echo 'error: libsignal-debug NOT found in build that SHOULD have it!' >&2
exit 2
fi
fi
}
# usage: build_desktop_for_arch target_triple host_triple output_dir
@ -186,12 +206,24 @@ export CXXFLAGS="-DOPENSSL_SMALL -flto=full ${CXXFLAGS:-}"
export CARGO_PROFILE_RELEASE_LTO=fat
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1
# Instruct boring-sys to autolink the *static* libc++, and to delay that linking until the final
# build product (the -bundle part). This is consistent with Google's advice for Android JNI
# libraries that are not part of the main app build[1], elaborated on in a GitHub thread[2]. It's
# also what we're doing with WebRTC. The syntax comes from rustc[3] via Cargo's rustc-link-lib build
# script feature.
#
# [1]: https://developer.android.com/ndk/guides/middleware-vendors#using_the_stl
# [2]: https://github.com/android/ndk/issues/796
# [3]: https://doc.rust-lang.org/rustc/command-line-arguments.html#-l-link-the-generated-crate-to-a-native-library
export BORING_BSSL_RUST_CPPLIB="static:-bundle=c++"
# Use the Android NDK's prebuilt Clang+lld as pqcrypto's compiler and Rust's linker.
ANDROID_TOOLCHAIN_DIR=$(echo "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt"/*/bin/)
export CC_aarch64_linux_android="${ANDROID_TOOLCHAIN_DIR}/aarch64-linux-android21-clang"
export CC_armv7_linux_androideabi="${ANDROID_TOOLCHAIN_DIR}/armv7a-linux-androideabi21-clang"
export CC_x86_64_linux_android="${ANDROID_TOOLCHAIN_DIR}/x86_64-linux-android21-clang"
export CC_i686_linux_android="${ANDROID_TOOLCHAIN_DIR}/i686-linux-android21-clang"
ANDROID_MIN_SDK_VERSION=23
export CC_aarch64_linux_android="${ANDROID_TOOLCHAIN_DIR}/aarch64-linux-android${ANDROID_MIN_SDK_VERSION}-clang"
export CC_armv7_linux_androideabi="${ANDROID_TOOLCHAIN_DIR}/armv7a-linux-androideabi${ANDROID_MIN_SDK_VERSION}-clang"
export CC_x86_64_linux_android="${ANDROID_TOOLCHAIN_DIR}/x86_64-linux-android${ANDROID_MIN_SDK_VERSION}-clang"
export CC_i686_linux_android="${ANDROID_TOOLCHAIN_DIR}/i686-linux-android${ANDROID_MIN_SDK_VERSION}-clang"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${CC_aarch64_linux_android}"
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="${CC_armv7_linux_androideabi}"
@ -225,6 +257,7 @@ target_for_abi() {
esac
}
for abi in "${android_abis[@]}"; do
rust_target=$(target_for_abi "$abi")
echo_then_run cargo build -p libsignal-jni -p libsignal-jni-testing ${RUST_RELEASE:+--release} ${FEATURES:+--features "${FEATURES[*]}"} -Z unstable-options --target "$rust_target" --artifact-dir "${ANDROID_LIB_DIR}/$abi" --timings

View File

@ -67,19 +67,18 @@ def current_origin_main_entry() -> Optional[Mapping[str, Any]]:
runs_info = subprocess.run(['gh', 'api', '--method=GET', f'repos/{repo_path}/actions/runs', '-f', f'head_sha={most_recent_commit}'], capture_output=True, check=True).stdout
runs_json = json.loads(runs_info)
run_id = [run['id'] for run in runs_json['workflow_runs'] if run['name'] == 'Build and Test'][0]
run_id = [run['id'] for run in runs_json['workflow_runs'] if 'Build and Test' in run['name']][0]
run_jobs = subprocess.run(['gh', 'run', 'view', '-R', repo_path, f'{run_id}', '--json', 'jobs'], capture_output=True, check=True).stdout
jobs_json = json.loads(run_jobs)
job_id = [job['databaseId'] for job in jobs_json['jobs'] if job['name'] == 'Java'][0]
job_id = [job['databaseId'] for job in jobs_json['jobs'] if job['name'] == 'Java Android'][0]
job_logs = subprocess.run(['gh', 'run', 'view', '-R', repo_path, '--job', f'{job_id}', '--log'], capture_output=True, check=True).stdout.decode()
for line in job_logs.splitlines():
if 'check_code_size.py' in line and 'current: *' in line:
(_, after) = line.split('(', maxsplit=1)
(bytes_count, _) = after.split(' bytes)', maxsplit=1)
if 'update code_size.json with' in line:
(_, bytes_count) = line.rsplit(' ', maxsplit=1)
return {'size': int(bytes_count), 'version': f'{most_recent_commit[:6]} ({base_ref})'}
print(f'skipping checking current {base_ref} (most recent run did not include check_code_size.py)', file=sys.stderr)

View File

@ -14,8 +14,9 @@ plugins {
id 'org.jetbrains.kotlin.jvm'
}
sourceCompatibility = 17
archivesBaseName = "libsignal-client"
base {
archivesName = "libsignal-client"
}
repositories {
mavenCentral()
@ -52,6 +53,7 @@ dependencies {
testImplementation 'junit:junit:4.13'
testImplementation 'com.googlecode.json-simple:json-simple:1.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
testImplementation 'org.jetbrains.kotlin:kotlin-test:2.1.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
}
@ -61,14 +63,15 @@ test {
testLogging {
events 'passed','skipped','failed'
showStandardStreams = true
showExceptions true
exceptionFormat 'full'
showCauses true
showStackTraces true
showExceptions = true
exceptionFormat = 'full'
showCauses = true
showStackTraces = true
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
withSourcesJar()
}
@ -84,14 +87,14 @@ sourcesJar {
}
task dokkaHtmlJar(type: Jar) {
dependsOn(dokkaHtml)
from(dokkaHtml)
dependsOn(dokkaGeneratePublicationHtml)
from(dokkaGeneratePublicationHtml)
archiveClassifier.set("dokka")
}
task dokkaJavadocJar(type: Jar) {
dependsOn(dokkaJavadoc)
from(dokkaJavadoc)
dependsOn(dokkaGeneratePublicationJavadoc)
from(dokkaGeneratePublicationJavadoc)
archiveClassifier.set("javadoc")
}
@ -110,13 +113,13 @@ processResources {
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = archivesBaseName
artifactId = base.archivesName.get()
from components.java
artifact dokkaHtmlJar
artifact dokkaJavadocJar
pom {
name = archivesBaseName
name = base.archivesName.get()
description = 'Signal Protocol cryptography library for Java'
url = 'https://github.com/signalapp/libsignal'
@ -145,6 +148,6 @@ publishing {
setUpSigningKey(signing)
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask(":client:publish") }
required = { isReleaseBuild() && gradle.taskGraph.hasTask(":client:publish") }
sign publishing.publications.mavenJava
}

View File

@ -11,6 +11,13 @@ import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
/**
* Implements the <a
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)">AES-256-CTR</a>
* stream cipher with a 12-byte nonce and an initial counter.
*
* <p>CTR mode is built on XOR, so encrypting and decrypting are the same operation.
*/
public class Aes256Ctr32 extends NativeHandleGuard.SimpleOwner {
public Aes256Ctr32(byte[] key, byte[] nonce, int initialCtr) throws InvalidKeyException {
super(
@ -23,10 +30,20 @@ public class Aes256Ctr32 extends NativeHandleGuard.SimpleOwner {
Native.Aes256Ctr32_Destroy(nativeHandle);
}
/**
* Encrypts the plaintext, or decrypts the ciphertext, in {@code data}, in place, advancing the
* state of the cipher.
*/
public void process(byte[] data) {
this.process(data, 0, data.length);
}
/**
* Encrypts the plaintext, or decrypts the ciphertext, in {@code data}, in place, advancing the
* state of the cipher.
*
* <p>Bytes outside the designated offset/length are unchanged.
*/
public void process(byte[] data, int offset, int length) {
guardedRun((nativeHandle) -> Native.Aes256Ctr32_Process(nativeHandle, data, offset, length));
}

View File

@ -11,9 +11,27 @@ import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
/**
* Implements the <a
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM)">AES-256-GCM</a>
* authenticated stream cipher with a 12-byte nonce.
*
* <p>This API exposes the streaming nature of AES-GCM to allow decrypting data without having it
* resident in memory all at once. You <strong>must</strong> call {@link #verifyTag} when the
* decryption is complete, or else you have no authenticity guarantees.
*
* @see Aes256GcmEncryption
*/
public class Aes256GcmDecryption extends NativeHandleGuard.SimpleOwner {
/** The size of the authentication tag, as used by {@link #verifyTag} */
public static final int TAG_SIZE_IN_BYTES = 16;
/**
* Initializes the cipher with the given inputs.
*
* <p>The associated data is not included in the plaintext or tag; instead, it's expected to match
* between the encrypter and decrypter.
*/
public Aes256GcmDecryption(byte[] key, byte[] nonce, byte[] associatedData)
throws InvalidKeyException {
super(
@ -27,18 +45,35 @@ public class Aes256GcmDecryption extends NativeHandleGuard.SimpleOwner {
Native.Aes256GcmDecryption_Destroy(nativeHandle);
}
public void decrypt(byte[] plaintext) {
/**
* Decrypts {@code ciphertext} in place and advances the state of the cipher.
*
* <p>Don't forget to call {@link #verifyTag} when decryption is complete.
*/
public void decrypt(byte[] ciphertext) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmDecryption_Update(nativeHandle, plaintext, 0, plaintext.length));
Native.Aes256GcmDecryption_Update(nativeHandle, ciphertext, 0, ciphertext.length));
}
public void decrypt(byte[] plaintext, int offset, int length) {
/**
* Decrypts {@code ciphertext} in place and advances the state of the cipher.
*
* <p>Bytes outside the designated offset/length are unchanged.
*
* <p>Don't forget to call {@link #verifyTag} when decryption is complete.
*/
public void decrypt(byte[] ciphertext, int offset, int length) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmDecryption_Update(nativeHandle, plaintext, offset, length));
Native.Aes256GcmDecryption_Update(nativeHandle, ciphertext, offset, length));
}
/**
* Returns {@code true} if and only if {@code tag} matches the ciphertext that has been processed.
*
* <p>After calling {@code verifyTag}, this object may not be used anymore.
*/
public boolean verifyTag(byte[] tag) {
return guardedMap(
(nativeHandle) ->

View File

@ -11,7 +11,25 @@ import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
/**
* Implements the <a
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM)">AES-256-GCM</a>
* authenticated stream cipher with a 12-byte nonce.
*
* <p>This API exposes the streaming nature of AES-GCM to allow encrypting data without having it
* resident in memory all at once. You must call {@link #computeTag} when the encryption is complete
* to produce the authentication tag for the ciphertext, and then make sure the tag makes it to the
* decrypter.
*
* @see Aes256GcmDecryption
*/
public class Aes256GcmEncryption extends NativeHandleGuard.SimpleOwner {
/**
* Initializes the cipher with the given inputs.
*
* <p>The associated data is not included in the plaintext or tag; instead, it's expected to match
* between the encrypter and decrypter. If you don't need any extra data, pass an empty array.
*/
public Aes256GcmEncryption(byte[] key, byte[] nonce, byte[] associatedData)
throws InvalidKeyException {
super(
@ -25,18 +43,35 @@ public class Aes256GcmEncryption extends NativeHandleGuard.SimpleOwner {
Native.Aes256GcmEncryption_Destroy(nativeHandle);
}
/**
* Encrypts {@code plaintext} in place and advances the state of the cipher.
*
* <p>Bytes outside the designated offset/length are unchanged.
*
* <p>Don't forget to call {@link #computeTag} when encryption is complete.
*/
public void encrypt(byte[] plaintext, int offset, int length) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmEncryption_Update(nativeHandle, plaintext, offset, length));
}
/**
* Encrypts {@code plaintext} in place and advances the state of the cipher.
*
* <p>Don't forget to call {@link #computeTag} when encryption is complete.
*/
public void encrypt(byte[] plaintext) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmEncryption_Update(nativeHandle, plaintext, 0, plaintext.length));
}
/**
* Produces an authentication tag for the plaintext that has been processed.
*
* <p>After calling {@code computeTag}, this object may not be used anymore.
*/
public byte[] computeTag() {
return guardedMap(Native::Aes256GcmEncryption_ComputeTag);
}

View File

@ -12,6 +12,13 @@ import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidMessageException;
/**
* Implements the <a href="https://en.wikipedia.org/wiki/AES-GCM-SIV">AES-256-GCM-SIV</a>
* authenticated stream cipher with a 12-byte nonce.
*
* <p>AES-GCM-SIV is a multi-pass algorithm (to generate the "synthetic initialization vector"), so
* this API does not expose a streaming form.
*/
public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
public Aes256GcmSiv(byte[] key) throws InvalidKeyException {
@ -23,6 +30,15 @@ public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
Native.Aes256GcmSiv_Destroy(nativeHandle);
}
/**
* Encrypts the given plaintext using the given nonce, and authenticating the ciphertext and given
* associated data.
*
* <p>The associated data is not included in the ciphertext; instead, it's expected to match
* between the encrypter and decrypter. If you don't need any extra data, pass an empty array.
*
* @return The encrypted data, including an appended 16-byte authentication tag.
*/
public byte[] encrypt(byte[] plaintext, byte[] nonce, byte[] associated_data) {
return filterExceptions(
() ->
@ -31,6 +47,15 @@ public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
Native.Aes256GcmSiv_Encrypt(nativeHandle, plaintext, nonce, associated_data)));
}
/**
* Decrypts the given ciphertext using the given nonce, and authenticating the ciphertext and
* given associated data.
*
* <p>The associated data is not included in the ciphertext; instead, it's expected to match
* between the encrypter and decrypter.
*
* @return The decrypted data
*/
public byte[] decrypt(byte[] ciphertext, byte[] nonce, byte[] associated_data)
throws InvalidMessageException {
return filterExceptions(

View File

@ -60,9 +60,19 @@ public fun <T, R> CompletableFuture<T>.mapWithCancellation(
this.whenComplete { value, err ->
when (err) {
null -> outer.complete(onSuccess(value))
null ->
try {
outer.complete(onSuccess(value))
} catch (e: Exception) {
outer.completeExceptionally(e)
}
is CancellationException -> outer.cancel(true)
else -> outer.complete(onError(err))
else ->
try {
outer.complete(onError(err))
} catch (e: Exception) {
outer.completeExceptionally(e)
}
}
}

View File

@ -5,8 +5,10 @@
package org.signal.libsignal.keytrans;
import org.signal.libsignal.net.BadRequestError;
/** Key transparency operation failed. */
public class KeyTransparencyException extends Exception {
public class KeyTransparencyException extends Exception implements BadRequestError {
public KeyTransparencyException(String message) {
super(message);
}

View File

@ -0,0 +1,133 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.messagebackup
import org.signal.libsignal.internal.Native
import org.signal.libsignal.internal.NativeHandleGuard
/**
* Result of exporting a single backup frame to JSON.
*
* @property line The JSON line for this frame, or null if the frame was filtered out (e.g.
* disappearing messages).
* @property errorMessage A validation error message, or null if the frame validated cleanly.
*/
public data class FrameExportResult(
val line: String?,
val errorMessage: String?,
)
/**
* Exports a backup to newline-delimited JSON (JSONL), frame by frame.
*
* Optionally validates each frame and the whole backup during export. Sanitization (filtering
* disappearing messages, stripping view-once attachments) is always applied.
*
* This class is not thread-safe.
*
* Example:
* ```
* val (exporter, initialChunk) = BackupJsonExporter.start(backupInfoBytes)
* exporter.use {
* output.write(initialChunk)
* output.write("\n")
* while (hasMoreFrames) {
* val results = exporter.exportFrames(framedBytes)
* for (result in results) {
* result.line?.let { output.write(it); output.write("\n") }
* result.errorMessage?.let { log.warn(it) }
* }
* }
* val finishError = exporter.finishExport()
* finishError?.let { log.warn(it) }
* }
* ```
*/
public class BackupJsonExporter private constructor(
private val handleOwner: NativeHandleGuard.CloseableOwner,
) : AutoCloseable {
private var closed = false
public companion object {
/**
* Creates a new exporter from serialized BackupInfo protobuf bytes.
*
* @param backupInfo serialized BackupInfo protobuf (without varint length prefix)
* @param validate whether to run semantic validation during export (default true)
* @return a pair of the exporter and the initial JSON chunk (serialized BackupInfo);
* caller must [close] the exporter when done
* @throws ValidationError if the BackupInfo is malformed
*/
@JvmStatic
@JvmOverloads
@Throws(ValidationError::class)
public fun start(
backupInfo: ByteArray,
validate: Boolean = true,
): Pair<BackupJsonExporter, String> {
val owner =
object : NativeHandleGuard.CloseableOwner(
Native.BackupJsonExporter_New(backupInfo, validate),
) {
override fun release(nativeHandle: Long) {
Native.BackupJsonExporter_Destroy(nativeHandle)
}
}
val initialChunk =
owner.guardedMap { h ->
Native.BackupJsonExporter_GetInitialChunk(h)
}
return Pair(BackupJsonExporter(owner), initialChunk)
}
}
/**
* Exports one or more varint-delimited Frame protobuf messages to JSON lines.
*
* Can be called repeatedly to stream frames through the exporter.
*
* @param frames one or more varint-delimited serialized Frame protobufs
* @return one result per frame, in order
* @throws ValidationError if the frame bytes cannot be parsed at all
* @throws IllegalStateException if the exporter has already been closed
*/
@Throws(ValidationError::class)
public fun exportFrames(frames: ByteArray): List<FrameExportResult> {
check(!closed) { "BackupJsonExporter is already closed" }
@Suppress("UNCHECKED_CAST")
val pairs =
handleOwner.guardedMapChecked { h -> Native.BackupJsonExporter_ExportFrames(h, frames) }
as Array<Pair<String?, String?>>
return pairs.map { (line, errorMessage) -> FrameExportResult(line, errorMessage) }
}
/**
* Completes the export and runs any final whole-backup validation checks.
*
* Must be called before [close]. Calling this on a closed exporter throws
* [IllegalStateException].
*
* @return a validation error message if whole-backup checks failed, or null if clean
* @throws IllegalStateException if the exporter has already been closed
*/
public fun finishExport(): String? {
check(!closed) { "BackupJsonExporter is already closed" }
return try {
handleOwner.guardedRunChecked { h -> Native.BackupJsonExporter_Finish(h) }
null
} catch (e: ValidationError) {
// All of our ValidationError instances should have a message, but we'll be defensive
// and provide a default message if one is not available.
e.message ?: "Backup export validation failed for unknown reason"
}
}
/** Closes the exporter, releasing native resources. Safe to call multiple times. */
override fun close() {
closed = true
handleOwner.close()
}
}

View File

@ -10,9 +10,9 @@ import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import java.io.IOException;
import java.io.InputStream;
import java.util.function.Supplier;
import kotlin.Pair;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.util.Pair;
/** Message-backup-related functionality. */
public class MessageBackup {
@ -73,11 +73,11 @@ public class MessageBackup {
result = outputPair;
}
String errorMessage = result.first();
String errorMessage = result.getFirst();
if (errorMessage != null) {
throw new ValidationError(errorMessage, result.second());
throw new ValidationError(errorMessage, result.getSecond());
}
return new ValidationResult(result.second());
return new ValidationResult(result.getSecond());
}
}

View File

@ -7,7 +7,6 @@ package org.signal.libsignal.metadata;
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -60,27 +59,19 @@ public class SealedSessionCipher {
SignalProtocolAddress destinationAddress,
SenderCertificate senderCertificate,
byte[] paddedPlaintext)
throws InvalidKeyException, UntrustedIdentityException {
try (NativeHandleGuard addressGuard = new NativeHandleGuard(destinationAddress)) {
CiphertextMessage message =
filterExceptions(
InvalidKeyException.class,
UntrustedIdentityException.class,
() ->
Native.SessionCipher_EncryptMessage(
paddedPlaintext,
addressGuard.nativeHandle(),
this.signalProtocolStore,
this.signalProtocolStore,
Instant.now().toEpochMilli()));
UnidentifiedSenderMessageContent content =
new UnidentifiedSenderMessageContent(
message,
senderCertificate,
UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT,
Optional.<byte[]>empty());
return encrypt(destinationAddress, content);
}
throws InvalidKeyException, NoSessionException, UntrustedIdentityException {
SignalProtocolAddress localAddress =
new SignalProtocolAddress(this.localUuidAddress, this.localDeviceId);
CiphertextMessage message =
new SessionCipher(signalProtocolStore, localAddress, destinationAddress)
.encrypt(paddedPlaintext);
UnidentifiedSenderMessageContent content =
new UnidentifiedSenderMessageContent(
message,
senderCertificate,
UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT,
Optional.<byte[]>empty());
return encrypt(destinationAddress, content);
}
public byte[] encrypt(
@ -95,7 +86,7 @@ public class SealedSessionCipher {
Native.SealedSessionCipher_Encrypt(
addressGuard.nativeHandle(),
contentGuard.nativeHandle(),
this.signalProtocolStore));
SessionCipher._bridge(this.signalProtocolStore)));
}
}
@ -173,7 +164,7 @@ public class SealedSessionCipher {
recipientSessionHandles,
ServiceId.toConcatenatedFixedWidthBinary(excludedRecipients),
contentGuard.nativeHandle(),
this.signalProtocolStore));
SessionCipher._bridge(this.signalProtocolStore)));
// Manually keep the lists of recipients and sessions from being garbage collected
// while we're using their native handles.
Native.keepAlive(recipients);
@ -204,7 +195,8 @@ public class SealedSessionCipher {
try {
content =
new UnidentifiedSenderMessageContent(
Native.SealedSessionCipher_DecryptToUsmc(ciphertext, this.signalProtocolStore));
Native.SealedSessionCipher_DecryptToUsmc(
ciphertext, SessionCipher._bridge(this.signalProtocolStore)));
validator.validate(content.getSenderCertificate(), timestamp);
} catch (Exception e) {
throw new InvalidMetadataMessageException(e);
@ -248,11 +240,13 @@ public class SealedSessionCipher {
}
public int getSessionVersion(SignalProtocolAddress remoteAddress) {
return new SessionCipher(signalProtocolStore, remoteAddress).getSessionVersion();
return new SessionCipher(signalProtocolStore, localAddress(), remoteAddress)
.getSessionVersion();
}
public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
return new SessionCipher(signalProtocolStore, remoteAddress).getRemoteRegistrationId();
return new SessionCipher(signalProtocolStore, localAddress(), remoteAddress)
.getRemoteRegistrationId();
}
private byte[] decrypt(UnidentifiedSenderMessageContent message)
@ -271,10 +265,10 @@ public class SealedSessionCipher {
switch (message.getType()) {
case CiphertextMessage.WHISPER_TYPE:
return new SessionCipher(signalProtocolStore, sender)
return new SessionCipher(signalProtocolStore, localAddress(), sender)
.decrypt(new SignalMessage(message.getContent()));
case CiphertextMessage.PREKEY_TYPE:
return new SessionCipher(signalProtocolStore, sender)
return new SessionCipher(signalProtocolStore, localAddress(), sender)
.decrypt(new PreKeySignalMessage(message.getContent()));
case CiphertextMessage.SENDERKEY_TYPE:
return new GroupCipher(signalProtocolStore, sender).decrypt(message.getContent());
@ -348,4 +342,8 @@ public class SealedSessionCipher {
return groupId;
}
}
private SignalProtocolAddress localAddress() {
return new SignalProtocolAddress(this.localUuidAddress, this.localDeviceId);
}
}

View File

@ -0,0 +1,139 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.internal.CompletableFuture
import org.signal.libsignal.internal.Native
import org.signal.libsignal.internal.mapWithCancellation
import org.signal.libsignal.protocol.ServiceId
public class AuthMessagesService(
private val connection: AuthenticatedChatConnection,
) {
/**
* Get an attachment upload form
*
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
* [RequestResult.ApplicationError]. A [UploadTooLargeException] means that the uploadSize was
* too large.
*/
public fun getUploadForm(uploadSize: Long): CompletableFuture<RequestResult<UploadForm, UploadTooLargeException>> =
try {
require(uploadSize >= 0, { "uploadSize ($uploadSize) wasn't >= 0" })
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
Native
.AuthenticatedChatConnection_get_upload_form(
asyncCtx,
conn,
uploadSize,
).mapWithCancellation(
onSuccess = { RequestResult.Success(it as UploadForm) },
onError = { err -> err.toRequestResult<UploadTooLargeException>() },
)
}
} catch (e: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
}
/**
* Sends an unsealed 1:1 message.
*
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
* [RequestResult.ApplicationError]. A [MismatchedDeviceException] indicates the recipient
* devices specified in `contents` are out of date in some way. (This is not a "partial success"
* result; the message has not been sent to anybody.) A [ServiceIdNotFoundException] indicates the
* destination account has been unregistered. A [RateLimitChallengeException] must be handled
* before the client can send this message.
*/
public fun sendMessage(
destination: ServiceId,
timestamp: Long,
contents: List<SingleOutboundUnsealedMessage>,
onlineOnly: Boolean,
urgent: Boolean,
): CompletableFuture<RequestResult<Unit, UnsealedSendFailure>> =
try {
val deviceIds = IntArray(contents.size)
val registrationIds = IntArray(contents.size)
val messages = arrayOfNulls<Object>(contents.size)
contents.forEachIndexed { i, next ->
deviceIds[i] = next.deviceId
registrationIds[i] = next.registrationId
messages[i] = next.message as Object
}
connection
.runWithContextAndConnectionHandles { asyncCtx, conn ->
Native.AuthenticatedChatConnection_send_message_java(
asyncCtx,
conn,
destination.toServiceIdFixedWidthBinary(),
timestamp,
deviceIds,
registrationIds,
messages.requireNoNulls(),
onlineOnly,
urgent,
)
}.mapWithCancellation(
onSuccess = { _ -> RequestResult.Success(Unit) },
onError = { err -> err.toRequestResult<UnsealedSendFailure>() },
)
} catch (e: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
}
/**
* Sends a 1:1 message to linked devices.
*
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
* [RequestResult.ApplicationError]. A [MismatchedDeviceException] indicates the recipient
* devices specified in `contents` are out of date in some way. (This is not a "partial success"
* result; the message has not been sent to anybody.) A [RateLimitChallengeException] must be
* handled before the client can send this message.
*/
public fun sendSyncMessage(
timestamp: Long,
contents: List<SingleOutboundUnsealedMessage>,
urgent: Boolean,
): CompletableFuture<RequestResult<Unit, SyncSendFailure>> =
try {
val deviceIds = IntArray(contents.size)
val registrationIds = IntArray(contents.size)
val messages = arrayOfNulls<Object>(contents.size)
contents.forEachIndexed { i, next ->
deviceIds[i] = next.deviceId
registrationIds[i] = next.registrationId
messages[i] = next.message as Object
}
connection
.runWithContextAndConnectionHandles { asyncCtx, conn ->
Native.AuthenticatedChatConnection_send_sync_message_java(
asyncCtx,
conn,
timestamp,
deviceIds,
registrationIds,
messages.requireNoNulls(),
urgent,
)
}.mapWithCancellation(
onSuccess = { _ -> RequestResult.Success(Unit) },
onError = { err -> err.toRequestResult<SyncSendFailure>() },
)
} catch (e: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
}
}
/** Either [ServiceIdNotFoundException], [RateLimitChallengeException], or [MismatchedDeviceException]. */
public sealed interface UnsealedSendFailure : BadRequestError
/** Either [RateLimitChallengeException] or [MismatchedDeviceException]. */
public sealed interface SyncSendFailure : BadRequestError

View File

@ -6,12 +6,12 @@
package org.signal.libsignal.net;
import java.util.Locale;
import kotlin.Pair;
import org.signal.libsignal.internal.CompletableFuture;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeTesting;
import org.signal.libsignal.internal.TokioAsyncContext;
import org.signal.libsignal.net.internal.BridgeChatListener;
import org.signal.libsignal.protocol.util.Pair;
/**
* Represents an authenticated communication channel with the ChatConnection.
@ -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

@ -64,21 +64,18 @@ public abstract class ChatConnection extends NativeHandleGuard.SimpleOwner {
this.chat = new WeakReference<>(chat);
}
public void onIncomingMessage(
public void receivedIncomingMessage(
byte[] envelope, long serverDeliveryTimestamp, long sendAckHandle) {
var ack = new ChatConnectionListener.ServerMessageAck(sendAckHandle);
ChatConnection chat = this.chat.get();
if (chat == null) return;
if (chat.chatListener == null) return;
chat.chatListener.onIncomingMessage(
chat,
envelope,
serverDeliveryTimestamp,
new ChatConnectionListener.ServerMessageAck(chat.tokioAsyncContext, sendAckHandle));
chat.chatListener.onIncomingMessage(chat, envelope, serverDeliveryTimestamp, ack);
}
public void onQueueEmpty() {
public void receivedQueueEmpty() {
ChatConnection chat = this.chat.get();
if (chat == null) return;
if (chat.chatListener == null) return;
@ -86,7 +83,7 @@ public abstract class ChatConnection extends NativeHandleGuard.SimpleOwner {
chat.chatListener.onQueueEmpty(chat);
}
public void onReceivedAlerts(String[] alerts) {
public void receivedAlerts(String[] alerts) {
ChatConnection chat = this.chat.get();
if (chat == null) return;
if (chat.chatListener == null) return;
@ -94,7 +91,7 @@ public abstract class ChatConnection extends NativeHandleGuard.SimpleOwner {
chat.chatListener.onReceivedAlerts(chat, alerts);
}
public void onConnectionInterrupted(Throwable disconnectReason) {
public void connectionInterrupted(Throwable disconnectReason) {
ChatConnection chat = this.chat.get();
if (chat == null) return;
if (chat.chatListener == null) return;
@ -119,19 +116,20 @@ public abstract class ChatConnection extends NativeHandleGuard.SimpleOwner {
void setChat(ChatConnection chat) {
this.chat = new WeakReference<>(chat);
if (savedAlerts != null) {
super.onReceivedAlerts(savedAlerts);
super.receivedAlerts(savedAlerts);
savedAlerts = null;
}
}
public void onReceivedAlerts(String[] alerts) {
@Override
public void receivedAlerts(String[] alerts) {
// This callback can happen before setChat, so we might need to replay it later.
if (this.chat.get() == null) {
savedAlerts = alerts;
return;
}
super.onReceivedAlerts(alerts);
super.receivedAlerts(alerts);
}
}

View File

@ -8,7 +8,6 @@ package org.signal.libsignal.net;
import org.signal.libsignal.internal.FilterExceptions;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.internal.TokioAsyncContext;
public interface ChatConnectionListener {
/**
@ -53,11 +52,8 @@ public interface ChatConnectionListener {
ChatConnection chat, ChatServiceException disconnectReason) {}
public static class ServerMessageAck extends NativeHandleGuard.SimpleOwner {
private final TokioAsyncContext asyncContext;
ServerMessageAck(TokioAsyncContext context, long nativeHandle) {
ServerMessageAck(long nativeHandle) {
super(nativeHandle);
asyncContext = context;
}
protected void release(long nativeHandle) {

View File

@ -5,14 +5,17 @@
package org.signal.libsignal.net;
import java.util.UUID;
import kotlin.Pair;
import org.signal.libsignal.internal.CompletableFuture;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.internal.NativeTesting;
import org.signal.libsignal.internal.TokioAsyncContext;
import org.signal.libsignal.net.ChatConnection.InternalRequest;
import org.signal.libsignal.protocol.util.Pair;
class FakeChatRemote extends NativeHandleGuard.SimpleOwner {
public static UUID FAKE_AUTH_CONNECT_SELF_UUID = new UUID(~0, ~0);
private TokioAsyncContext tokioContext;
FakeChatRemote(TokioAsyncContext tokioContext, long nativeHandle) {
@ -32,7 +35,7 @@ class FakeChatRemote extends NativeHandleGuard.SimpleOwner {
.thenApply(
rawRequest -> {
var sentRequest = (Pair<Long, Long>) rawRequest;
return new Pair(new InternalRequest(sentRequest.first()), sentRequest.second());
return new Pair(new InternalRequest(sentRequest.getFirst()), sentRequest.getSecond());
});
}

View File

@ -1,20 +0,0 @@
//
// Copyright 2025 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
public abstract class KeyTransparency {
/**
* Mode of the monitor operation.
*
* <p>If the newer version of account data is found in the key transparency log, self-monitor will
* terminate with an error, but monitor for other account will fall back to a full search and
* update the locally stored data.
*/
public enum MonitorMode {
SELF,
OTHER
}
}

View File

@ -0,0 +1,79 @@
//
// Copyright 2025 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
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.
*
* The behavior of [KeyTransparencyClient.check] differs depending on whether it is
* performed for the owner of the account or contact and in the former case whether
* the phone number discoverability is enabled.
*
* For example, if the newer version of account data is found in the key
* transparency log while monitoring "self", it will terminate with an error.
* However, the same check for a "contact" will result in a follow-up search
* operation.
*/
public sealed class CheckMode {
public data class Self(
val isE164Discoverable: Boolean,
) : CheckMode()
public object Contact : CheckMode()
public fun isE164Discoverable(): Boolean? =
when (this) {
is Self -> isE164Discoverable
is Contact -> null
}
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

@ -1,298 +0,0 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
import java.util.Optional;
import org.signal.libsignal.internal.CompletableFuture;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.internal.TokioAsyncContext;
import org.signal.libsignal.keytrans.Store;
import org.signal.libsignal.net.KeyTransparency.MonitorMode;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ServiceId;
/**
* Typed API to access the key transparency subsystem using an existing unauthenticated chat
* connection.
*
* <p>Unlike {@link ChatConnection}, key transparency client does not export "raw" send/receive
* APIs, and instead uses them internally to implement high-level operations.
*
* <p>Note: {@code Store} APIs may be invoked concurrently. Here are possible strategies to make
* sure there are no thread safety violations:
*
* <ul>
* <li>Types implementing {@code Store} can be made thread safe
* <li>{@link KeyTransparencyClient} operations-completed asynchronous calls-can be serialized.
* </ul>
*
* <p>Example usage:
*
* <pre>
* var net = new Network(Network.Environment.STAGING, "key-transparency-example");
* var chat = net.connectUnauthChat(new Listener()).get();
* chat.start();
*
* KeyTransparencyClient client = chat.keyTransparencyClient();
*
* client.search(aci, identityKey, null, null, null, KT_DATA_STORE).get();
*
* </pre>
*/
public class KeyTransparencyClient {
private final TokioAsyncContext tokioAsyncContext;
private final UnauthenticatedChatConnection chatConnection;
private final Network.Environment environment;
KeyTransparencyClient(
UnauthenticatedChatConnection chat,
TokioAsyncContext tokioAsyncContext,
Network.Environment environment) {
this.chatConnection = chat;
this.tokioAsyncContext = tokioAsyncContext;
this.environment = environment;
}
/**
* Search for account information in the key transparency tree.
*
* <p>Only ACI and ACI identity key are required to identify the account.
*
* <p>If the latest distinguished tree head is not present in the store, it will be requested from
* the server prior to performing the search via {@link #updateDistinguished}.
*
* <p>This is an asynchronous operation; all the exceptions occurring during communication with
* the server will be wrapped in {@link java.util.concurrent.ExecutionException}.
*
* <p>Possible exceptions include:
*
* <ul>
* <li>{@link ChatServiceException} for errors related to communication with the server.
* Depending on the concrete subclass, client can retry the operation. See also {@link
* TimeoutException}, {@link ServerSideErrorException}, {@link UnexpectedResponseException},
* {@link AppExpiredException},
* <li>{@link RetryLaterException} when the client is being throttled. Wait for the specified
* amount of time before making new requests.
* <li>{@link NetworkException} when the network operation fails. Clients can retry the call
* after a recommended period.
* <li>{@link NetworkProtocolException} when a high level network protocol fails. For example,
* failure to establish a websocket connection.
* <li>{@link org.signal.libsignal.keytrans.KeyTransparencyException} for errors related to key
* transparency logic, which includes missing required fields in the serialized data.
* Retrying the search without changing any of the arguments (including the state of the
* store) is unlikely to yield a different result.
* <li>{@link org.signal.libsignal.keytrans.VerificationFailedException} indicates a failure to
* verify the data in key transparency server response, such as an incorrect proof or a
* wrong signature.
* </ul>
*
* @param aci the ACI of the account to be searched for. Required.
* @param aciIdentityKey {@link IdentityKey} associated with the ACI. Required.
* @param e164 string representation of an E.164 number associated with the account. Optional.
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
* same optionality as the E.164 parameter.
* @param usernameHash hash of the username associated with the account. Optional.
* @param store local persistent storage for key transparency-related data, such as the latest
* tree heads and account monitoring data. It will be queried for data before performing the
* server request and updated with the latest information from the server response if it
* succeeds.
* @return an instance of {@link CompletableFuture} successful completion of which will indicate
* that the search request has succeeded and store has been updated with the latest account
* data.
* @throws IllegalArgumentException if the store contains corrupted data.
*/
public CompletableFuture<Void> search(
/* @NotNull */ final ServiceId.Aci aci,
/* @NotNull */ final IdentityKey aciIdentityKey,
final String e164,
final byte[] unidentifiedAccessKey,
final byte[] usernameHash,
final Store store) {
Optional<byte[]> lastDistinguishedTreeHead = store.getLastDistinguishedTreeHead();
if (lastDistinguishedTreeHead.isEmpty()) {
return this.updateDistinguished(store)
.thenCompose(
(ignored) ->
this.search(
aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, store));
}
// Decoding of the last distinguished tree head happens "eagerly" before making any network
// requests.
// It may result in an IllegalArgumentException.
try (NativeHandleGuard tokioContextGuard = this.tokioAsyncContext.guard();
NativeHandleGuard identityKeyGuard = aciIdentityKey.getPublicKey().guard();
NativeHandleGuard chatConnectionGuard = new NativeHandleGuard(chatConnection); ) {
return Native.KeyTransparency_Search(
tokioContextGuard.nativeHandle(),
this.environment.value,
chatConnectionGuard.nativeHandle(),
aci.toServiceIdFixedWidthBinary(),
identityKeyGuard.nativeHandle(),
e164,
unidentifiedAccessKey,
usernameHash,
store.getAccountData(aci).orElse(null),
lastDistinguishedTreeHead.get())
.thenApply(
(accountData) -> {
store.setAccountData(aci, accountData);
return null;
});
}
}
/**
* Request the latest distinguished tree head from the server and update it in the local store.
*
* <p>This is an asynchronous operation; all the exceptions occurring during communication with
* the server will be wrapped in {@link java.util.concurrent.ExecutionException}.
*
* <p>Possible exceptions include:
*
* <ul>
* <li>{@link ChatServiceException} for errors related to communication with the server.
* Depending on the concrete subclass, client can retry the operation. See also {@link
* TimeoutException}, {@link ServerSideErrorException}, {@link UnexpectedResponseException},
* {@link AppExpiredException},
* <li>{@link RetryLaterException} when the client is being throttled. Wait for the specified
* amount of time before making new requests.
* <li>{@link NetworkException} when the network operation fails. Clients can retry the call
* after a recommended period.
* <li>{@link NetworkProtocolException} when a high level network protocol fails. For example,
* failure to establish a websocket connection.
* <li>{@link org.signal.libsignal.keytrans.KeyTransparencyException} for errors related to key
* transparency logic. Retrying the search without changing any of the arguments (including
* the state of the store) is unlikely to yield a different result.
* </ul>
*
* @param store local persistent storage for key transparency related data, such as the latest
* tree heads and account monitoring data. It will be queried for the latest distinguished
* tree head before performing the server request and updated with data from the server
* response if it succeeds. Distinguished tree does not have to be present in the store prior
* to the call.
* @return An instance of {@link CompletableFuture} representing the asynchronous operation, which
* does not produce any value. Successful completion of the operation results in an updated
* state of the store.
* @throws IllegalArgumentException if the store contains corrupted data.
*/
public CompletableFuture<Void> updateDistinguished(final Store store) {
byte[] lastDistinguished = store.getLastDistinguishedTreeHead().orElse(null);
try (NativeHandleGuard tokioContextGuard = this.tokioAsyncContext.guard();
NativeHandleGuard chatConnectionGuard = new NativeHandleGuard(chatConnection)) {
return Native.KeyTransparency_Distinguished(
tokioContextGuard.nativeHandle(),
this.environment.value,
chatConnectionGuard.nativeHandle(),
lastDistinguished)
.thenApply(
bytes -> {
store.setLastDistinguishedTreeHead(bytes);
return null;
});
}
}
/**
* Issue a monitor request to the key transparency service.
*
* <p>Store must contain data associated with the account being requested prior to making this
* call. Another way of putting this is: monitor cannot be called before {@link #search}.
*
* <p>If any of the monitored fields in the server response contain a version that is higher than
* the one currently in the store, the behavior depends on the mode parameter value.
*
* <ul>
* <li>{@code MonitorMode.SELF} - An exception will be thrown, no search request will be issued.
* <li>{@code MonitorMode.OTHER} - A search request will be performed automatically and, if it
* succeeds, the updated account data will be stored.
* </ul>
*
* <p>If the latest distinguished tree head is not present in the store, it will be requested from
* the server prior to performing the search via {@link #updateDistinguished}.
*
* <p>This is an asynchronous operation; all the exceptions occurring during communication with
* the server will be wrapped in {@link java.util.concurrent.ExecutionException}.
*
* <p>Possible exceptions include:
*
* <ul>
* <li>{@link ChatServiceException} for errors related to communication with the server.
* Depending on the concrete subclass, client can retry the operation. See also {@link
* TimeoutException}, {@link ServerSideErrorException}, {@link UnexpectedResponseException},
* {@link AppExpiredException},
* <li>{@link RetryLaterException} when the client is being throttled. Wait for the specified
* amount of time before making new requests.
* <li>{@link NetworkException} when the network operation fails. Clients can retry the call
* after a recommended period.
* <li>{@link NetworkProtocolException} when a high level network protocol fails. For example,
* failure to establish a websocket connection.
* <li>{@link org.signal.libsignal.keytrans.KeyTransparencyException} for errors related to key
* transparency logic, which includes missing required fields in the serialized data.
* Retrying the search without changing any of the arguments (including the state of the
* store) is unlikely to yield a different result.
* <li>{@link org.signal.libsignal.keytrans.VerificationFailedException} indicates a failure to
* verify the data in key transparency server response, such as an incorrect proof or a
* wrong signature.
* </ul>
*
* @param mode Mode of the monitor operation. See {@link MonitorMode}.
* @param aci the ACI of the account to be searched for. Required.
* @param aciIdentityKey {@link IdentityKey} associated with the ACI. Required.
* @param e164 string representation of an E.164 number associated with the account. Optional.
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
* same optionality as the E.164 parameter.
* @param usernameHash hash of the username associated with the account. Optional.
* @param store local persistent storage for key transparency-related data, such as the latest
* tree heads and account monitoring data. It will be queried for data before performing the
* server request and updated with the latest information from the server response if it
* succeeds.
* @return an instance of {@link CompletableFuture} successful completion of which will indicate
* that the monitor request has succeeded and store has been updated with the latest account
* data.
* @throws IllegalArgumentException if the store contains corrupted data.
*/
public CompletableFuture<Void> monitor(
/* @NotNull */ final MonitorMode mode,
final ServiceId.Aci aci,
/* @NotNull */ final IdentityKey aciIdentityKey,
final String e164,
final byte[] unidentifiedAccessKey,
final byte[] usernameHash,
final Store store) {
Optional<byte[]> lastDistinguishedTreeHead = store.getLastDistinguishedTreeHead();
if (lastDistinguishedTreeHead.isEmpty()) {
return this.updateDistinguished(store)
.thenCompose(
(ignored) ->
this.monitor(
mode, aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, store));
}
try (NativeHandleGuard tokioContextGuard = this.tokioAsyncContext.guard();
NativeHandleGuard identityKeyGuard = aciIdentityKey.getPublicKey().guard();
NativeHandleGuard chatConnectionGuard = new NativeHandleGuard(chatConnection)) {
return Native.KeyTransparency_Monitor(
tokioContextGuard.nativeHandle(),
this.environment.value,
chatConnectionGuard.nativeHandle(),
aci.toServiceIdFixedWidthBinary(),
identityKeyGuard.nativeHandle(),
e164,
unidentifiedAccessKey,
usernameHash,
// Technically this is a required parameter, but passing null
// to generate the error on the Rust side.
store.getAccountData(aci).orElse(null),
lastDistinguishedTreeHead.get(),
mode == MonitorMode.SELF)
.thenApply(
(updatedAccountData) -> {
store.setAccountData(aci, updatedAccountData);
return null;
});
}
}
}

View File

@ -0,0 +1,154 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.internal.CompletableFuture
import org.signal.libsignal.internal.Native
import org.signal.libsignal.internal.NativeHandleGuard
import org.signal.libsignal.internal.TokioAsyncContext
import org.signal.libsignal.internal.mapWithCancellation
import org.signal.libsignal.keytrans.KeyTransparencyException
import org.signal.libsignal.keytrans.Store
import org.signal.libsignal.keytrans.VerificationFailedException
import org.signal.libsignal.net.KeyTransparency.CheckMode
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.ServiceId
/**
* Typed API to access the key transparency subsystem using an existing unauthenticated chat
* connection.
*
* Unlike [ChatConnection], key transparency client does not export "raw" send/receive APIs, and
* instead uses them internally to implement high-level operations.
*
* All operations return [RequestResult]. Request-specific failures are represented as
* [RequestResult.NonSuccess] with [KeyTransparencyException]; retryable network errors as
* [RequestResult.RetryableNetworkError].
*
* Note: [Store] APIs may be invoked concurrently. Here are possible strategies to make sure there
* are no thread safety violations:
* - Types implementing [Store] can be made thread safe
* - [KeyTransparencyClient] operations-completed asynchronous calls-can be serialized.
*
* Example usage:
* ```
* val net = Network(Network.Environment.STAGING, "key-transparency-example")
* val chat = net.connectUnauthChat(Listener()).get()
* chat.start()
*
* val client = chat.keyTransparencyClient()
*
* val result = client.check(CheckMode.Contact, aci, identityKey, null, null, null, KT_DATA_STORE).get()
* ```
*/
public class KeyTransparencyClient internal constructor(
private val chatConnection: UnauthenticatedChatConnection,
private val tokioAsyncContext: TokioAsyncContext,
private val environment: Network.Environment,
) {
/**
* A unified key transparency operation that performs a search, a monitor, or both.
*
* Caller should pass latest known values of all identifiers (ACI, E.164, username hash) associated
* with the account, along with a correct value of [CheckMode].
*
* If there is no data in the store for the account, the search operation will be performed. Following
* this initial search, the monitor operation will be used.
*
* If any of the fields in the monitor response contain a version that is higher than the one
* currently in the store, the behavior depends on the mode parameter value.
* - [CheckMode.Self] - A [KeyTransparencyException] will be returned, no search request will
* be issued.
* - [CheckMode.Contact] - Another search request will be performed automatically and, if it succeeds,
* the updated account data will be stored.
*
* Possible non-success results include:
* - [RequestResult.RetryableNetworkError] for errors related to communication with the server,
* including [RetryLaterException] when the client is being throttled,
* [ServerSideErrorException], [NetworkException], [NetworkProtocolException], and
* [TimeoutException].
* - [RequestResult.NonSuccess] with [KeyTransparencyException] for errors related to key
* transparency logic, which includes missing required fields in the serialized data.
* Retrying without changing any of the arguments (including the state of the store) is
* unlikely to yield a different result.
* - [RequestResult.NonSuccess] with [VerificationFailedException] (a subclass of
* [KeyTransparencyException]) indicating a failure to verify the data in key transparency
* server response, such as an incorrect proof or a wrong signature.
* - [RequestResult.ApplicationError] for invalid arguments or other caller errors that could have
* been avoided, such as providing an [unidentifiedAccessKey] without an [e164].
*
* @param mode Mode of the key transparency operation being performed. See [CheckMode].
* @param aci the ACI of the account to be checked. Required.
* @param aciIdentityKey [IdentityKey] associated with the ACI. Required.
* @param e164 string representation of an E.164 number associated with the account. Optional.
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
* same optionality as the E.164 parameter.
* @param usernameHash hash of the username associated with the account. Optional.
* @param store local persistent storage for key transparency-related data, such as the latest
* tree heads and account monitoring data. It will be queried for data before performing the
* server request and updated with the latest information from the server response if it
* succeeds.
* @return an instance of [CompletableFuture] that completes with a [RequestResult] indicating
* success or containing the error details.
*/
public fun check(
mode: CheckMode,
aci: ServiceId.Aci,
aciIdentityKey: IdentityKey,
e164: String?,
unidentifiedAccessKey: ByteArray?,
usernameHash: ByteArray?,
store: Store,
): CompletableFuture<RequestResult<Unit, KeyTransparencyException>> {
val lastDistinguishedTreeHead =
try {
store.lastDistinguishedTreeHead
} catch (t: Throwable) {
return CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
}
return try {
NativeHandleGuard(tokioAsyncContext).use { tokioContextGuard ->
NativeHandleGuard(aciIdentityKey.publicKey).use { identityKeyGuard ->
NativeHandleGuard(chatConnection).use { chatConnectionGuard ->
Native
.KeyTransparency_Check(
tokioContextGuard.nativeHandle(),
environment.value,
chatConnectionGuard.nativeHandle(),
aci.toServiceIdFixedWidthBinary(),
identityKeyGuard.nativeHandle(),
e164,
unidentifiedAccessKey,
usernameHash,
// Technically this is a required parameter, but passing null
// to generate the error on the Rust side.
store.getAccountData(aci).orElse(null),
lastDistinguishedTreeHead.orElse(null),
mode.isSelf(),
mode.isE164Discoverable() ?: true,
).mapWithCancellation(
onSuccess = { (updatedAccountData, distinguished) ->
try {
store.setAccountData(aci, updatedAccountData)
if (distinguished.isNotEmpty()) {
store.setLastDistinguishedTreeHead(distinguished)
}
RequestResult.Success(Unit)
} catch (t: Throwable) {
RequestResult.ApplicationError(t)
}
},
onError = { err -> err.toRequestResult<KeyTransparencyException>() },
)
}
}
}
} catch (t: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
}
}
}

View File

@ -20,7 +20,10 @@ import java.io.IOException
*/
public class MismatchedDeviceException :
IOException,
MultiRecipientSendFailure {
MultiRecipientSendFailure,
SealedSendFailure,
SyncSendFailure,
UnsealedSendFailure {
public data class Entry(
public val account: ServiceId,
public val missingDevices: IntArray = intArrayOf(),
@ -58,6 +61,7 @@ public class MismatchedDeviceException :
public val entries: Array<Entry>
@CalledFromNative
public constructor(message: String, entries: Array<Entry>) : super(message) {
this.entries = entries
}

View File

@ -318,8 +318,9 @@ public class Network {
* to the chat service, and incoming events will be provided via the provided {@link
* ChatConnectionListener} argument.
*
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException} or
* other exception type wrapped in a {@link ExecutionException}.
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException},
* {@link PossibleCaptiveNetworkException}, or other exception type wrapped in a {@link
* ExecutionException}.
*/
public CompletableFuture<UnauthenticatedChatConnection> connectUnauthChat(
final Locale locale, ChatConnectionListener listener) {
@ -344,8 +345,9 @@ public class Network {
* the chat service, and incoming events will be provided via the provided {@link
* ChatConnectionListener} argument.
*
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException} or
* other exception type wrapped in a {@link ExecutionException}.
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException},
* {@link PossibleCaptiveNetworkException}, or other exception type wrapped in a {@link
* ExecutionException}.
*/
public CompletableFuture<AuthenticatedChatConnection> connectAuthChat(
final String username,
@ -375,6 +377,17 @@ public class Network {
: new String[] {locale.getLanguage() + "-" + locale.getCountry()};
}
/**
* Creates a new instance of {@link ProvisioningConnection}.
*
* <p>If the connection attempt fails, the future will contain a {@link ChatServiceException} or
* other exception type wrapped in a {@link ExecutionException}.
*/
public CompletableFuture<ProvisioningConnection> connectProvisioning(
ProvisioningConnectionListener listener) {
return ProvisioningConnection.connect(tokioAsyncContext, connectionManager, listener);
}
static class ConnectionManager extends NativeHandleGuard.SimpleOwner
implements ConnectChatBridge {
private final Environment environment;

View File

@ -0,0 +1,15 @@
//
// Copyright 2025 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
/**
* Indicates that a server presented a TLS certificate that might have come from a captive network.
*/
public class PossibleCaptiveNetworkException extends NetworkException {
public PossibleCaptiveNetworkException(String message) {
super(message);
}
}

View File

@ -0,0 +1,172 @@
//
// Copyright 2025 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
import java.lang.ref.WeakReference;
import kotlin.Pair;
import org.signal.libsignal.internal.CompletableFuture;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.internal.NativeTesting;
import org.signal.libsignal.internal.TokioAsyncContext;
import org.signal.libsignal.net.internal.BridgeProvisioningListener;
/**
* A chat connection used specifically for provisioning linked devices.
*
* <p>Note that no messages are sent *from* the client for a provisioning connection; all the
* interesting functionality is in the events delivered to the {@link
* ProvisioningConnectionListener}.
*/
public class ProvisioningConnection extends NativeHandleGuard.SimpleOwner {
private final TokioAsyncContext tokioAsyncContext;
private final ProvisioningConnectionListener listener;
protected ProvisioningConnection(
final TokioAsyncContext tokioAsyncContext,
final long nativeHandle,
final ProvisioningConnectionListener listener) {
super(nativeHandle);
this.tokioAsyncContext = tokioAsyncContext;
this.listener = listener;
}
static CompletableFuture<ProvisioningConnection> connect(
final TokioAsyncContext tokioAsyncContext,
final Network.ConnectionManager connectionManager,
final ProvisioningConnectionListener listener) {
return tokioAsyncContext.guardedMap(
asyncContextHandle ->
connectionManager.guardedMap(
connectionManagerHandle ->
Native.ProvisioningChatConnection_connect(
asyncContextHandle, connectionManagerHandle)
.makeCancelable(tokioAsyncContext)
.thenApply(
nativeHandle ->
new ProvisioningConnection(
tokioAsyncContext, nativeHandle, listener))));
}
protected static class ListenerBridge implements BridgeProvisioningListener {
// Stored as a weak reference because otherwise we'll have a reference cycle:
// - After setting a listener, Rust has a GC GlobalRef to this ListenerBridge
// - This field is a normal Java reference to the ProvisioningConnection
// - ProvisioningConnection owns the Rust ProvisioningConnection object
protected WeakReference<ProvisioningConnection> connection;
protected ListenerBridge(ProvisioningConnection connection) {
this.connection = new WeakReference<>(connection);
}
public void receivedAddress(String address, long sendAckHandle) {
var ack = new ChatConnectionListener.ServerMessageAck(sendAckHandle);
ProvisioningConnection connection = this.connection.get();
if (connection == null) return;
if (connection.listener == null) return;
connection.listener.onReceivedAddress(connection, address, ack);
}
public void receivedEnvelope(byte[] envelope, long sendAckHandle) {
var ack = new ChatConnectionListener.ServerMessageAck(sendAckHandle);
ProvisioningConnection connection = this.connection.get();
if (connection == null) return;
if (connection.listener == null) return;
connection.listener.onReceivedEnvelope(connection, envelope, ack);
}
public void connectionInterrupted(Throwable disconnectReason) {
ProvisioningConnection connection = this.connection.get();
if (connection == null) return;
if (connection.listener == null) return;
ChatServiceException disconnectReasonChatServiceException =
(disconnectReason == null)
? null
: (disconnectReason instanceof ChatServiceException)
? (ChatServiceException) disconnectReason
: new ChatServiceException("OtherDisconnectReason", disconnectReason);
connection.listener.onConnectionInterrupted(connection, disconnectReasonChatServiceException);
}
}
protected static final class SetChatLaterListenerBridge extends ListenerBridge {
SetChatLaterListenerBridge() {
super(null);
}
void setChat(ProvisioningConnection connection) {
this.connection = new WeakReference<>(connection);
}
}
/**
* Test-only method to create a {@code ProvisioningConnection} connected to a fake remote.
*
* <p>The returned {@link FakeChatRemote} can be used to send messages to the connection.
*/
public static Pair<ProvisioningConnection, FakeChatRemote> fakeConnect(
final TokioAsyncContext tokioAsyncContext, ProvisioningConnectionListener listener) {
return tokioAsyncContext.guardedMap(
asyncContextHandle -> {
SetChatLaterListenerBridge bridgeListener = new SetChatLaterListenerBridge();
long fakeChatConnection =
NativeTesting.TESTING_FakeChatConnection_CreateProvisioning(
asyncContextHandle, bridgeListener);
ProvisioningConnection chat =
new ProvisioningConnection(
tokioAsyncContext,
NativeTesting.TESTING_FakeChatConnection_TakeProvisioningChat(fakeChatConnection),
listener);
bridgeListener.setChat(chat);
FakeChatRemote fakeRemote =
new FakeChatRemote(
tokioAsyncContext,
NativeTesting.TESTING_FakeChatConnection_TakeRemote(fakeChatConnection));
NativeTesting.FakeChatConnection_Destroy(fakeChatConnection);
return new Pair<>(chat, fakeRemote);
});
}
/**
* Starts a created, but not yet active, provisioning connection.
*
* <p>This must be called on a new {@code ProvisioningConnection} before it can start receiving
* incoming messages from the server. It is an error to call this method more than once on a
* {@code ProvisioningConnection}.
*/
public void start() {
ListenerBridge bridgedListener = new ListenerBridge(this);
this.guardedRun(
nativeChatConnectionHandle ->
Native.ProvisioningChatConnection_init_listener(
nativeChatConnectionHandle, bridgedListener));
}
/**
* Initiates termination of the underlying connection to the Chat Service. After the service is
* disconnected, it cannot be reconnected.
*
* @return a future that completes when the underlying connection is terminated.
*/
@SuppressWarnings("unchecked")
public CompletableFuture<Void> disconnect() {
return tokioAsyncContext.guardedMap(
asyncContextHandle ->
guardedMap(
chatConnectionHandle ->
Native.ProvisioningChatConnection_disconnect(
asyncContextHandle, chatConnectionHandle)));
}
@Override
protected void release(long nativeChatConnectionHandle) {
Native.ProvisioningChatConnection_Destroy(nativeChatConnectionHandle);
}
}

View File

@ -0,0 +1,40 @@
//
// Copyright 2025 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
public interface ProvisioningConnectionListener {
/**
* Called at the start of the provisioning process.
*
* <p>{@param address} should be considered an opaque token to pass to the primary device (usually
* via QR code).
*
* <p>{@param ack}'s {@code send} method can be called immediately to indicate successful delivery
* of the address.
*/
void onReceivedAddress(
ProvisioningConnection chat, String address, ChatConnectionListener.ServerMessageAck ack);
/**
* Called once when the primary sends an "envelope" via the server (using the address from {@link
* #onReceivedAddress(String, ChatConnectionListener.ServerMessageAck)}).
*
* <p>Once the server receives the {@param ack} for this message, it will close this connection.
*/
void onReceivedEnvelope(
ProvisioningConnection chat, byte[] envelope, ChatConnectionListener.ServerMessageAck ack);
/**
* Called when the client gets disconnected from the server.
*
* <p>This includes both deliberate disconnects as well as unexpected socket closures. In the case
* of the former, the {@param disconnectReason} will be null.
*
* <p>The default implementation of this method does nothing.
*/
default void onConnectionInterrupted(
ProvisioningConnection chat, ChatServiceException disconnectReason) {}
}

View File

@ -6,6 +6,7 @@
package org.signal.libsignal.net
import org.signal.libsignal.internal.CalledFromNative
import java.time.Duration
import java.util.EnumSet
/**
@ -14,13 +15,37 @@ import java.util.EnumSet
* <p>When the websocket transport is in use, this corresponds to a {@code HTTP 428} response to
* requests to a number of endpoints.
*/
public class RateLimitChallengeException : ChatServiceException {
public class RateLimitChallengeException :
ChatServiceException,
BadRequestError,
SyncSendFailure,
UnsealedSendFailure {
public val token: String
public val options: Set<ChallengeOption>
public val retryLater: Duration?
@CalledFromNative
public constructor(message: String, token: String, options: Array<ChallengeOption>) : super(message) {
public constructor(
message: String,
token: String,
options: Array<ChallengeOption>,
retryLater: Duration?,
) : super(message) {
this.token = token
this.options = EnumSet.copyOf(options.asList())
this.retryLater = retryLater
}
@CalledFromNative
internal constructor(
message: String,
token: String,
options: Array<ChallengeOption>,
retryLater: Long,
) : this(
message,
token,
options,
if (retryLater < 0) null else Duration.ofSeconds(retryLater),
) {
}
}

View File

@ -33,6 +33,8 @@ import org.signal.libsignal.protocol.kem.KEMPublicKey;
* <li>{@link RegistrationSessionNotFoundException} if the server rejects the session ID,
* <li>{@link ChatServiceException} if a request times out after being sent to the server,
* <li>{@link RetryLaterException} if the server responds with an HTTP 429,
* <li>{@link PossibleCaptiveNetworkException} if the server's TLS response suggests a captive
* network.
* <li>{@link RegistrationSessionIdInvalidException} if the session ID is invalid,
* <li>{@link RegistrationException} for other unexpected error responses
* </ul>

View File

@ -54,6 +54,8 @@ public sealed interface RequestResult<out T, out E : BadRequestError> {
*
* Possible types for [networkError] include but are not limited to:
* - [TimeoutException]: occurs when the request takes too long to complete.
* - [ChatServiceInactiveException]: occurs when the request is made on a closed chat
* connection.
* - [ConnectedElsewhereException]: occurs when a client connects elsewhere with
* same credentials before the request could complete
* - [ConnectionInvalidatedException]: occurs when the connection to the server is
@ -110,6 +112,7 @@ internal inline fun <reified E : BadRequestError> Throwable.toRequestResult(): R
internal fun Throwable.toRequestResult(): RequestResult<Nothing, Nothing> =
when (this) {
is TimeoutException -> RequestResult.RetryableNetworkError(this, null)
is ChatServiceInactiveException -> RequestResult.RetryableNetworkError(this)
is ConnectedElsewhereException -> RequestResult.RetryableNetworkError(this)
// ConnectionInvalidated is mapped to a network error. Only one legacy API uses its
// specific meaning; all other APIs treat it as a generic network error.
@ -126,9 +129,9 @@ internal fun Throwable.toRequestResult(): RequestResult<Nothing, Nothing> =
*
* This extension function handles the case where the Future itself fails
* (as opposed to the request returning an error result). Any exceptions
* thrown while waiting for the Future are converted to [ApplicationError].
* thrown while waiting for the Future are converted to [ApplicationError][RequestResult.ApplicationError].
*
* @return The [RequestResult] from the Future, or [ApplicationError] if the
* @return The [RequestResult] from the Future, or [ApplicationError][RequestResult.ApplicationError] if the
* Future failed to complete normally
*/
public fun <T, E : BadRequestError> CompletableFuture<RequestResult<T, E>>.getOrError(): RequestResult<T, E> =

View File

@ -5,6 +5,7 @@
package org.signal.libsignal.net
import org.signal.libsignal.internal.CalledFromNative
import java.io.IOException
/**
@ -15,6 +16,11 @@ import java.io.IOException
*/
public class RequestUnauthorizedException :
IOException,
MultiRecipientSendFailure {
public constructor(message: String) : super(message) {}
GetPreKeysError,
GetUploadFormError,
MultiRecipientSendFailure,
SealedSendFailure {
@CalledFromNative
public constructor(message: String) : super(message) {
}
}

View File

@ -0,0 +1,25 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.internal.CalledFromNative
import java.io.IOException
/**
* A request relating to a [org.signal.libsignal.protocol.ServiceId] could not be completed as the
* ServiceId, or its devices, could not be found.
*
* See the specific request docs for more information.
*/
public class ServiceIdNotFoundException :
IOException,
GetPreKeysError,
SealedSendFailure,
UnsealedSendFailure {
@CalledFromNative
public constructor(message: String) : super(message) {
}
}

View File

@ -0,0 +1,22 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.protocol.message.CiphertextMessage
/**
* A message to send to a single device of a peer.
*
* Used by APIs like [UnauthMessagesService.sendMessage].
*/
public data class SingleOutboundMessage<T>(
public val deviceId: Int,
public val registrationId: Int,
public val message: T,
)
public typealias SingleOutboundSealedSenderMessage = SingleOutboundMessage<ByteArray>
public typealias SingleOutboundUnsealedMessage = SingleOutboundMessage<CiphertextMessage>

View File

@ -73,7 +73,7 @@ import org.signal.libsignal.messagebackup.BackupKey
* ```
*
* @see [BackupKey]
* @see [MessageBackupKey](org.signal.libsignal.messagebackup.MessageBackupKey)
* @see [MessageBackupKey][org.signal.libsignal.messagebackup.MessageBackupKey]
* @see [BackupForwardSecrecyToken]
*/
public class SvrB internal constructor(
@ -183,9 +183,9 @@ public class SvrB internal constructor(
* - [Result.failure] containing
* [InvalidSvrBDataException](org.signal.libsignal.svr.InvalidSvrBDataException) if the backup
* metadata is malformed. In this case the user's data is **not recoverable**.
* - [Result.failure] containing [RestoreFailedException] if restoration fails (with remaining
* - [Result.failure] containing [RestoreFailedException](org.signal.libsignal.svr.RestoreFailedException) if restoration fails (with remaining
* tries count). This should never happen but if it does the user's data is **not recoverable**.
* - [Result.failure] containing [DataMissingException] if the backup data is not found on the
* - [Result.failure] containing [DataMissingException](org.signal.libsignal.svr.DataMissingException) if the backup data is not found on the
* server, indicating an **incorrect backup key** (which may in turn imply the user's data is
* not recoverable).
* - [Result.failure] containing [RetryLaterException] if the server is rate limiting this client.
@ -308,7 +308,7 @@ private class BackupRestoreResponse internal constructor(
*/
public data class SvrBStoreResponse(
/**
* The forward secrecy token used to derive [MessageBackupKey] instances.
* The forward secrecy token used to derive [MessageBackupKey][org.signal.libsignal.messagebackup.MessageBackupKey] instances.
*
* This token provides forward secrecy guarantees by ensuring that compromise of the backup key
* alone is insufficient to decrypt backups. Each backup is protected by a value stored on
@ -344,7 +344,7 @@ public data class SvrBStoreResponse(
*/
public data class SvrBRestoreResponse(
/**
* The forward secrecy token used to derive [MessageBackupKey] instances.
* The forward secrecy token used to derive [MessageBackupKey][org.signal.libsignal.messagebackup.MessageBackupKey] instances.
*
* This token provides forward secrecy guarantees by ensuring that compromise of the backup key
* alone is insufficient to decrypt backups. Each backup is protected by a value stored on

View File

@ -0,0 +1,102 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.internal.CompletableFuture
import org.signal.libsignal.internal.Native
import org.signal.libsignal.internal.mapWithCancellation
import org.signal.libsignal.protocol.ecc.ECPrivateKey
import org.signal.libsignal.zkgroup.GenericServerPublicParams
import org.signal.libsignal.zkgroup.backups.BackupAuthCredential
/**
* Either a [RequestUnauthorizedException] or [UploadTooLargeException]
*/
public sealed interface GetUploadFormError : BadRequestError
public data class BackupAuth(
val credential: BackupAuthCredential,
val serverKeys: GenericServerPublicParams,
val signingKey: ECPrivateKey,
)
public data class DeterministicRandomSeedUseOnlyForTesting(
val seed: Long,
)
public class UnauthBackupsService(
private val connection: UnauthenticatedChatConnection,
) {
/**
* Get a messages backup attachment upload form
*
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
* [RequestResult.ApplicationError]. A [UploadTooLargeException] means that the uploadSize was
* too large. A [RequestUnauthorizedException] means that the authorization failed.
*/
public fun getUploadForm(
auth: BackupAuth,
uploadSize: Long,
rngSeedForTesting: DeterministicRandomSeedUseOnlyForTesting? = null,
): CompletableFuture<RequestResult<UploadForm, GetUploadFormError>> =
try {
require(uploadSize >= 0, { "uploadSize ($uploadSize) wasn't >= 0" })
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
auth.signingKey.guardedMap { signingKey ->
Native
.UnauthenticatedChatConnection_backup_get_upload_form(
asyncCtx,
conn,
auth.credential.internalContentsForJNI,
auth.serverKeys.internalContentsForJNI,
signingKey,
uploadSize,
rngSeedForTesting?.seed ?: -1,
).mapWithCancellation(
onSuccess = { RequestResult.Success(it as UploadForm) },
onError = { err -> err.toRequestResult<GetUploadFormError>() },
)
}
}
} catch (e: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
}
/**
* Get an attachment backup attachment upload form
*
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
* [RequestResult.ApplicationError]. A [UploadTooLargeException] means that the uploadSize was
* too large. A [RequestUnauthorizedException] means that the authorization failed.
*/
public fun getMediaUploadForm(
auth: BackupAuth,
uploadSize: Long,
rngSeedForTesting: DeterministicRandomSeedUseOnlyForTesting? = null,
): CompletableFuture<RequestResult<UploadForm, GetUploadFormError>> =
try {
require(uploadSize >= 0, { "uploadSize ($uploadSize) wasn't >= 0" })
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
auth.signingKey.guardedMap { signingKey ->
Native
.UnauthenticatedChatConnection_backup_get_media_upload_form(
asyncCtx,
conn,
auth.credential.internalContentsForJNI,
auth.serverKeys.internalContentsForJNI,
signingKey,
uploadSize,
rngSeedForTesting?.seed ?: -1,
).mapWithCancellation(
onSuccess = { RequestResult.Success(it as UploadForm) },
onError = { err -> err.toRequestResult<GetUploadFormError>() },
)
}
}
} catch (e: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
}
}

View File

@ -0,0 +1,103 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.internal.CompletableFuture
import org.signal.libsignal.internal.Native
import org.signal.libsignal.internal.mapWithCancellation
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.ServiceId
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.protocol.state.PreKeyBundle
public sealed class DeviceSpecifier {
public object AllDevices : DeviceSpecifier()
public data class SpecificDevice(
val deviceId: Int,
) : DeviceSpecifier()
}
public sealed interface GetPreKeysError : BadRequestError
public class UnauthKeysService(
private val connection: UnauthenticatedChatConnection,
) {
/**
* Fetch the prekeys for a given target user
*
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
* [RequestResult.ApplicationError]. A [RequestUnauthorizedException] means `auth` is not valid
* for the target. A [ServiceIdNotFoundException] means that the requested identity or device does
* not exist or device has no available prekeys.
*/
public fun getPreKeys(
target: ServiceId,
device: DeviceSpecifier,
auth: UserBasedAuthorization,
): CompletableFuture<RequestResult<Pair<IdentityKey, List<PreKeyBundle>>, GetPreKeysError>> {
val device =
when (device) {
is DeviceSpecifier.SpecificDevice -> {
require(device.deviceId >= 0)
device.deviceId
}
is DeviceSpecifier.AllDevices -> -1
}
return try {
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
// Suppress the warnings about java.lang.Object being inferred as the type
// parameter for mapWithCancellation
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
when (auth) {
is UserBasedAuthorization.AccessKey -> {
Native.UnauthenticatedChatConnection_get_pre_keys_access_key_auth(
asyncCtx,
conn,
auth.bytes,
target.toServiceIdFixedWidthBinary(),
device,
)
}
is UserBasedAuthorization.GroupSend -> {
Native.UnauthenticatedChatConnection_get_pre_keys_group_auth(
asyncCtx,
conn,
auth.token.serialize(),
target.toServiceIdFixedWidthBinary(),
device,
)
}
is UserBasedAuthorization.UnrestrictedUnauthenticatedAccess -> {
Native.UnauthenticatedChatConnection_get_pre_keys_unrestricted_auth(
asyncCtx,
conn,
target.toServiceIdFixedWidthBinary(),
device,
)
}
}.mapWithCancellation(
onSuccess = { out: Any ->
val (publicKey, preKeyBundles) = out as Pair<*, *>
@Suppress("UNCHECKED_CAST") // The cast _is_ checked because Arrays don't use type erasure
RequestResult.Success(
Pair(
IdentityKey(publicKey as ECPublicKey),
(preKeyBundles as Array<PreKeyBundle>).toList(),
),
)
},
onError = { err -> err.toRequestResult<GetPreKeysError>() },
)
}
} catch (e: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
}
}
}

View File

@ -60,6 +60,61 @@ public class UnauthMessagesService(
} catch (e: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
}
/**
* Sends a 1:1 message encrypted with Sealed Sender.
*
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
* [RequestResult.ApplicationError]. A [RequestUnauthorizedException] means `auth` is not valid
* for `destination`; this cannot happen when `auth` is
* [UserBasedSendAuthorization.Story]. A [MismatchedDeviceException] indicates the recipient
* devices specified in `contents` are out of date in some way. (This is not a "partial success"
* result; the message has not been sent to anybody.) A [ServiceIdNotFoundException] indicates the
* destination account has been unregistered.
*
* @see [SealedSessionCipher.encrypt]
*/
public fun sendMessage(
destination: ServiceId,
timestamp: Long,
contents: List<SingleOutboundSealedSenderMessage>,
auth: UserBasedSendAuthorization,
onlineOnly: Boolean,
urgent: Boolean,
): CompletableFuture<RequestResult<Unit, SealedSendFailure>> =
try {
val deviceIds = IntArray(contents.size)
val registrationIds = IntArray(contents.size)
val messages = arrayOfNulls<ByteArray>(contents.size)
contents.forEachIndexed { i, next ->
deviceIds[i] = next.deviceId
registrationIds[i] = next.registrationId
messages[i] = next.message
}
connection
.runWithContextAndConnectionHandles { asyncCtx, conn ->
Native.UnauthenticatedChatConnection_send_message(
asyncCtx,
conn,
destination.toServiceIdFixedWidthBinary(),
timestamp,
deviceIds,
registrationIds,
messages.requireNoNulls(),
auth.rawKind(),
auth.payloadBytesOrNull(),
onlineOnly,
urgent,
)
}.mapWithCancellation(
onSuccess = { _ -> RequestResult.Success(Unit) },
onError = { err -> err.toRequestResult<SealedSendFailure>() },
)
} catch (e: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
}
}
public sealed interface MultiRecipientSendAuthorization {
@ -93,3 +148,28 @@ public class MultiRecipientMessageResponse(
rawUnregisteredIds: Array<ByteArray>,
) : this(rawUnregisteredIds.map(ServiceId::parseFromFixedWidthBinary)) {}
}
/** Either [UserBasedAuthorization], or `UserBasedSendAuthorization.Story`. */
public sealed interface UserBasedSendAuthorization {
public object Story : UserBasedSendAuthorization
}
// Must be kept in sync with `UserBasedSendAuthorizationKind` in Rust.
private fun UserBasedSendAuthorization.rawKind(): Int =
when (this) {
is UserBasedSendAuthorization.Story -> 0
is UserBasedAuthorization.AccessKey -> 1
is UserBasedAuthorization.GroupSend -> 2
is UserBasedAuthorization.UnrestrictedUnauthenticatedAccess -> 3
}
private fun UserBasedSendAuthorization.payloadBytesOrNull(): ByteArray? =
when (this) {
is UserBasedSendAuthorization.Story -> null
is UserBasedAuthorization.AccessKey -> bytes
is UserBasedAuthorization.GroupSend -> token.serialize()
is UserBasedAuthorization.UnrestrictedUnauthenticatedAccess -> null
}
/** Either [ServiceIdNotFoundException], [RequestUnauthorizedException] or [MismatchedDeviceException]. */
public sealed interface SealedSendFailure : BadRequestError

View File

@ -0,0 +1,38 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.internal.CompletableFuture
import org.signal.libsignal.internal.Native
import org.signal.libsignal.internal.mapWithCancellation
import org.signal.libsignal.protocol.ServiceId
public class UnauthProfilesService(
private val connection: UnauthenticatedChatConnection,
) {
/**
* Does an account with the given ACI or PNI exist?
*
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
* [RequestResult.ApplicationError].
*/
public fun accountExists(account: ServiceId): CompletableFuture<RequestResult<Boolean, Nothing>> =
try {
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
Native
.UnauthenticatedChatConnection_account_exists(
asyncCtx,
conn,
account.toServiceIdFixedWidthBinary(),
).mapWithCancellation(
onSuccess = { RequestResult.Success(it) },
onError = { err -> err.toRequestResult() },
)
}
} catch (e: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
}
}

View File

@ -60,7 +60,7 @@ public class UnauthUsernamesService(
if (pair == null) {
RequestResult.Success(null)
} else {
RequestResult.Success(Username._withPrecomputedHash(pair.first(), pair.second()))
RequestResult.Success(Username._withPrecomputedHash(pair.first, pair.second))
}
},
onError = { err -> err.toRequestResult<LookUpUsernameLinkFailure>() },

View File

@ -6,12 +6,12 @@
package org.signal.libsignal.net;
import java.util.Locale;
import kotlin.Pair;
import org.signal.libsignal.internal.CompletableFuture;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeTesting;
import org.signal.libsignal.internal.TokioAsyncContext;
import org.signal.libsignal.net.internal.BridgeChatListener;
import org.signal.libsignal.protocol.util.Pair;
/**
* Represents an unauthenticated (i.e. hopefully anonymous) communication channel with the Chat
@ -33,7 +33,7 @@ public class UnauthenticatedChatConnection extends ChatConnection {
this.keyTransparencyClient = new KeyTransparencyClient(this, tokioAsyncContext, ktEnvironment);
}
private KeyTransparencyClient keyTransparencyClient;
private final KeyTransparencyClient keyTransparencyClient;
static CompletableFuture<UnauthenticatedChatConnection> connect(
final TokioAsyncContext tokioAsyncContext,
@ -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

@ -0,0 +1,40 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.internal.CalledFromNative
import java.net.URI
import java.net.URISyntaxException
public data class UploadForm(
val cdn: Int,
val key: String,
val headers: Map<String, String>,
val signedUploadUrl: URI,
) {
public companion object {
@JvmStatic
@CalledFromNative
@Suppress("UNCHECKED_CAST")
private fun fromNative(
cdn: Int,
key: String,
headers: Array<*>,
signedUploadUrl: String,
): UploadForm =
UploadForm(
cdn = cdn,
key = key,
headers = (headers as Array<Pair<String, String>>).asList().toMap(),
signedUploadUrl =
try {
URI(signedUploadUrl)
} catch (_: URISyntaxException) {
throw UnexpectedResponseException("Invalid URL for UploadForm's signedUploadUrl")
},
)
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.libsignal.net
import org.signal.libsignal.internal.CalledFromNative
import java.io.IOException
/**
* The request size was larger than the maximum supported upload size
*
* See the specific request docs for more information.
*/
public class UploadTooLargeException :
IOException,
BadRequestError,
GetUploadFormError {
@CalledFromNative
public constructor(message: String) : super(message) {
}
}

View File

@ -0,0 +1,34 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken
public sealed class UserBasedAuthorization : UserBasedSendAuthorization {
public data class AccessKey(
val bytes: ByteArray,
) : UserBasedAuthorization() {
// Because the default equals+hashCode compare based on identity, not value
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AccessKey
if (!bytes.contentEquals(other.bytes)) return false
return true
}
override fun hashCode(): Int = bytes.contentHashCode()
}
public data class GroupSend(
val token: GroupSendFullToken,
) : UserBasedAuthorization()
public object UnrestrictedUnauthenticatedAccess : UserBasedAuthorization()
}

View File

@ -138,6 +138,45 @@ class AsyncTests {
}.await()
}
@Test
fun testMapWithCancellationOnSuccessException() =
runTest(timeout = 5.seconds) {
val baseFuture = CompletableFuture<String>()
val exception = RuntimeException("onSuccess error")
val mappedFuture =
baseFuture.mapWithCancellation(
onSuccess = { throw exception },
onError = { "error" },
)
baseFuture.complete("value")
// If mapWithCancellation doesn't handle exceptions from onSuccess,
// the outer future will never complete and this will time out.
val thrown = assertFailsWith<RuntimeException> { mappedFuture.await() }
assertEquals("onSuccess error", thrown.message)
}
@Test
fun testMapWithCancellationOnErrorException() =
runTest(timeout = 5.seconds) {
val baseFuture = CompletableFuture<String>()
val exception = RuntimeException("onError error")
val mappedFuture =
baseFuture.mapWithCancellation(
onSuccess = { "success" },
onError = { throw exception },
)
baseFuture.completeExceptionally(IllegalStateException("original"))
// Similar to above, if mapWithCancellation doesn't handle exceptions
// from onError, the outer future will never complete and this will
// time out.
val thrown = assertFailsWith<RuntimeException> { mappedFuture.await() }
assertEquals("onError error", thrown.message)
}
@Test
fun testToResultFutureSuccessPath() =
runTest {

View File

@ -11,16 +11,16 @@ import org.signal.libsignal.protocol.ServiceId;
public class TestStore implements Store {
public HashMap<ServiceId.Aci, Deque<byte[]>> storage = new HashMap<>();
public byte[] lastDistinguishedTreeHead;
public Deque<byte[]> distinguishedTreeHeads = new ArrayDeque<>();
@Override
public Optional<byte[]> getLastDistinguishedTreeHead() {
return Optional.ofNullable(lastDistinguishedTreeHead);
return Optional.ofNullable(this.distinguishedTreeHeads.peekLast());
}
@Override
public void setLastDistinguishedTreeHead(byte[] lastDistinguishedTreeHead) {
this.lastDistinguishedTreeHead = lastDistinguishedTreeHead;
this.distinguishedTreeHeads.push(lastDistinguishedTreeHead);
}
@Override

View File

@ -0,0 +1,206 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.messagebackup
import org.json.simple.JSONObject
import org.json.simple.parser.JSONParser
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.signal.libsignal.messagebackup.VarintDelimitedTestUtil.chunkLengthDelimited
import org.signal.libsignal.messagebackup.VarintDelimitedTestUtil.insertLengthPrefix
import org.signal.libsignal.messagebackup.VarintDelimitedTestUtil.stripLengthPrefix
import org.signal.libsignal.util.ResourceReader
import java.io.ByteArrayOutputStream
import kotlin.io.encoding.Base64
class BackupJsonExporterTest {
companion object {
private fun concatFrames(frames: List<ByteArray>): ByteArray =
frames.fold(ByteArrayOutputStream()) { out, frame -> out.also { it.write(frame) } }.toByteArray()
private val canonicalBackup: ByteArray by lazy {
ResourceReader.readAll(
BackupJsonExporterTest::class.java.getResourceAsStream("canonical-backup.binproto"),
)
}
// The canonical backup has 6 chunks: 1 BackupInfo + 5 frames.
private val allChunks by lazy { chunkLengthDelimited(canonicalBackup) }
private val backupInfo by lazy { stripLengthPrefix(allChunks.first()) }
private val frameChunks by lazy { allChunks.drop(1) }
// Disappearing chat item frame. Regenerate with:
// % protoc rust/message-backup/src/proto/backup.proto \
// --encode signal.backup.Frame <<'PROTO' | base64
// chatItem: { chatId: 1 authorId: 2 dateSent: 3 expiresInMs: 1 }
// PROTO
private val disappearingChatItemFrame: ByteArray =
Base64.decode("IggIARACGAMoAQ==")
// View-once chat item frame with revisions. Regenerate with:
// % protoc rust/message-backup/src/proto/backup.proto \
// --encode signal.backup.Frame <<'PROTO' | base64
// chatItem: {
// chatId: 10 authorId: 11 dateSent: 12
// viewOnceMessage: { attachment: { wasDownloaded: true } }
// revisions: [{ chatId: 10 authorId: 11 dateSent: 9
// viewOnceMessage: { attachment: { wasDownloaded: true } } }]
// }
// PROTO
private val viewOnceChatItemFrame: ByteArray =
Base64.decode("IhwIChALGAwyDQgKEAsYCZIBBAoCGAGSAQQKAhgB")
}
// These tests verify basic streaming behavior and the Kotlin API surface.
// More thorough JSON output validation is done in the Node.js tests.
@Test
fun streamsJsonLinesForCanonicalBackup() {
val (exporter, initialChunk) = BackupJsonExporter.start(backupInfo)
exporter.use {
val chunkGroups = listOf(frameChunks.take(2), frameChunks.drop(2))
val exportedLines =
chunkGroups.flatMap { exporter.exportFrames(concatFrames(it)) }.map {
assertNotNull("canonical backup should produce a line", it.line)
assertNull("canonical backup should validate cleanly", it.errorMessage)
it.line!!
}
assertNull("canonical backup should validate cleanly", exporter.finishExport())
val allLines = listOf(initialChunk) + exportedLines
assertEquals(frameChunks.size + 1, allLines.size)
for (line in allLines) {
assertFalse("each line should be a single line", line.contains('\n'))
assertTrue("each line should be JSON", line.startsWith("{"))
}
assertTrue(allLines[0].contains("\"version\""))
assertTrue(allLines[1].contains("\"account\""))
}
}
@Test
fun returnsEmptyListWhenNoFramesProvided() {
val (exporter, initialChunk) = BackupJsonExporter.start(backupInfo, validate = false)
exporter.use {
assertTrue(initialChunk.contains("\"version\""))
assertEquals(emptyList<FrameExportResult>(), exporter.exportFrames(ByteArray(0)))
assertNull(exporter.finishExport())
}
}
@Test
fun filtersDisappearingMessages() {
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = false)
exporter.use {
val results = exporter.exportFrames(insertLengthPrefix(disappearingChatItemFrame))
assertEquals(1, results.size)
assertNull(results[0].line)
assertNull(results[0].errorMessage)
assertNull(exporter.finishExport())
}
}
@Test
fun filteredFramesHaveNoValidationErrorWhenValid() {
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = true)
exporter.use {
val results = exporter.exportFrames(insertLengthPrefix(disappearingChatItemFrame))
assertEquals(1, results.size)
assertNull(results[0].line)
assertNull(results[0].errorMessage)
// Finish should report an error because we never sent an AccountData frame.
assertNotNull(exporter.finishExport())
}
}
@Test
fun stripsAttachmentsFromViewOnceMessages() {
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = false)
exporter.use {
val results = exporter.exportFrames(insertLengthPrefix(viewOnceChatItemFrame))
assertEquals(1, results.size)
assertNotNull(results[0].line)
assertNull(results[0].errorMessage)
val json = JSONParser().parse(results[0].line!!) as JSONObject
val expected =
JSONParser().parse(
"""
{
"chatItem": {
"chatId": "10",
"authorId": "11",
"dateSent": "12",
"viewOnceMessage": {},
"revisions": [
{
"chatId": "10",
"authorId": "11",
"dateSent": "9",
"viewOnceMessage": {}
}
]
}
}
""".trimIndent(),
) as JSONObject
assertEquals(expected, json)
assertNull(exporter.finishExport())
}
}
@Test
fun validationPassesWithNoErrorsPresent() {
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = true)
exporter.use {
for (group in listOf(frameChunks.take(1), frameChunks.drop(1))) {
for (result in exporter.exportFrames(concatFrames(group))) {
assertNotNull(result.line)
assertNull(result.errorMessage)
}
}
assertNull(exporter.finishExport())
}
}
@Test
fun finishReportsErrorWhenValidationFails() {
val (exporter, initialChunk) = BackupJsonExporter.start(backupInfo, validate = true)
exporter.use {
assertTrue(initialChunk.startsWith("{"))
// Skip the first frame (AccountData) to trigger a validation failure.
exporter.exportFrames(concatFrames(frameChunks.drop(1)))
val finishError = exporter.finishExport()
assertNotNull(finishError)
assertTrue(finishError!!.isNotEmpty())
}
}
@Test
fun canSkipValidation() {
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = false)
exporter.use {
val results = exporter.exportFrames(concatFrames(frameChunks.drop(1)))
for (result in results) {
assertNull(result.errorMessage)
}
assertNull(exporter.finishExport())
}
}
@Test(expected = ValidationError::class)
fun rejectsMalformedDataEvenWithoutValidation() {
val (exporter, _) = BackupJsonExporter.start(backupInfo, validate = false)
exporter.use {
exporter.exportFrames(byteArrayOf(0x02, 0x01))
}
}
}

View File

@ -19,12 +19,12 @@ import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import kotlin.io.encoding.Base64;
import org.junit.Test;
import org.signal.libsignal.protocol.ServiceId.Aci;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.signal.libsignal.protocol.util.Hex;
import org.signal.libsignal.util.Base64;
import org.signal.libsignal.util.ResourceReader;
public class MessageBackupValidationTest {
@ -95,23 +95,13 @@ public class MessageBackupValidationTest {
public void onlineValidation() throws IOException, ValidationError {
final InputStream input = ComparableBackupTest.getCanonicalBackupInputStream();
final int backupInfoLength = input.read();
assertFalse("unexpected EOF", backupInfoLength == -1);
assertTrue("single-byte varint", backupInfoLength < 0x80);
final int backupInfoLength = VarintDelimitedTestUtil.readVarint(input);
final byte[] backupInfo = new byte[backupInfoLength];
assertEquals("unexpected EOF", backupInfoLength, input.read(backupInfo));
final OnlineBackupValidator backup = new OnlineBackupValidator(backupInfo, BACKUP_PURPOSE);
int frameLength;
while ((frameLength = input.read()) != -1) {
// Tiny varint parser, only supports two bytes.
if (frameLength >= 0x80) {
final int secondByte = input.read();
assertFalse("unexpected EOF", secondByte == -1);
assertTrue("at most a two-byte varint", secondByte < 0x80);
frameLength -= 0x80;
frameLength |= secondByte << 7;
}
while ((frameLength = VarintDelimitedTestUtil.readVarint(input)) != -1) {
final byte[] frame = new byte[frameLength];
assertEquals("unexpected EOF", frameLength, input.read(frame));
backup.addFrame(frame);
@ -126,6 +116,10 @@ public class MessageBackupValidationTest {
ValidationError.class, () -> new OnlineBackupValidator(new byte[0], BACKUP_PURPOSE));
}
private static byte[] decodeBase64(String input) {
return Base64.Default.decode(input, 0, input.length());
}
// The following payload was generated via protoscope.
// % protoscope -s | base64
// The fields are described by Backup.proto.
@ -134,7 +128,7 @@ public class MessageBackupValidationTest {
// 2: 1731715200000
// 3: {`00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff`}
private static byte[] VALID_BACKUP_INFO =
Base64.decode("CAEQgOiTkrMyGiAAESIzRFVmd4iZqrvM3e7/ABEiM0RVZneImaq7zN3u/w==");
decodeBase64("CAEQgOiTkrMyGiAAESIzRFVmd4iZqrvM3e7/ABEiM0RVZneImaq7zN3u/w==");
@Test
public void onlineValidatorRejectsInvalidFrame() throws ValidationError {

View File

@ -0,0 +1,65 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.messagebackup
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import java.io.InputStream
import java.nio.ByteBuffer
/** Helpers for working with varint-length-delimited protobuf streams in tests. */
object VarintDelimitedTestUtil {
/**
* Reads a varint from [input], or returns -1 on EOF. Only supports up to two-byte varints.
*/
@JvmStatic
fun readVarint(input: InputStream): Int {
val first = input.read()
if (first == -1) return -1
if (first < 0x80) return first
val second = input.read()
assertFalse("unexpected EOF in middle of varint", second == -1)
assertTrue("at most a two-byte varint", second < 0x80)
return (first - 0x80) + (second shl 7)
}
// Tiny varint parser, only supports two bytes.
@JvmStatic
fun readVarint(buf: ByteBuffer): Int {
val first = buf.get().toInt() and 0xFF
if (first < 0x80) return first
val second = buf.get().toInt() and 0xFF
assertTrue("at most a two-byte varint", second < 0x80)
return (first - 0x80) + (second shl 7)
}
@JvmStatic
fun chunkLengthDelimited(data: ByteArray): List<ByteArray> {
val buf = ByteBuffer.wrap(data)
val chunks = mutableListOf<ByteArray>()
while (buf.hasRemaining()) {
val start = buf.position()
val length = readVarint(buf)
val end = buf.position() + length
buf.position(end)
chunks.add(data.copyOfRange(start, end))
}
return chunks
}
@JvmStatic
fun stripLengthPrefix(chunk: ByteArray): ByteArray {
val buf = ByteBuffer.wrap(chunk)
val length = readVarint(buf)
return chunk.copyOfRange(buf.position(), buf.position() + length)
}
@JvmStatic
fun insertLengthPrefix(data: ByteArray): ByteArray {
assertTrue("test frame too large for single-byte varint", data.size < 0x80)
return byteArrayOf(data.size.toByte()) + data
}
}

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