Optimize presentation of credentials (AuthCredentialPresentationV2, ProfileKeyCredentialPresentationV2, PniCredentialPresentationV2). Server will accept V1 or V2 presentations. Clients will produce V2.
Various improvements to FFI to support this, and some minor optimizations (in particular "lazy statics" to avoid redundant loading of SystemParams).
This was previously necessary because the Linux implementation of
SwiftPM+XCTest didn't support automatic test discovery, but that's no
longer a problem with newer versions of Swift.
In Java these are subclasses of IllegalStateException, a
RuntimeException, so that every session operation isn't annotated as
throwing InvalidSessionException. Swift and TypeScript don't have
typed errors, so they're just additional specific cases that can be
caught.
- Provide a fallback for generating random data that doesn't use
Security.framework.
- Fix package unsafe linker settings for ld.gold compatibility.
- Don't depend on the UUID_NULL constant.
Originally InvalidCiphertext meant something structurally wrong, while
InvalidMessage meant it wasn't decryptable. But distinguishing those
isn't really important except for debugging purposes, so using a
string description is sufficient.
Previously slice inputs expanded to two parameters in the C bridge: a
pointer and a length. This required extra support in the bridge_fn
macro and still needed Swift callers to pass the length explicitly
(with the potential for typos). Now, the Rust side uses a new
BorrowedSliceOf struct, which cbindgen monomorphizes to names like
'SignalBorrowedSliceOfProtocolAddress', plus and a special-case for
bytes: 'SignalBorrowedBuffer'. Together with some conveniences on the
Swift side, this makes calls safer and simpler with fewer special
cases. (In particular, we can now have variable-length input types
that aren't written syntactically as slices.)
Outputs still have the special bridge_fn_buffer mode, which provides
separate pointer and length output parameters. We can revisit this in
the future, but bridge_fn_buffer can save a copy in the Java and Node
bridges, and bridge_fn_void also exists today (for zero output
parameters in the C bridge when Rust has a Result return type).
- Fixes an issue where a call to decrypt would instead re-encrypt the
ciphertext.
- Tweaks the interface for the initializer to instead take raw public
key bytes. This matches the java interface. Framework clients have no
way to construct a PublicKey from raw bytes.
- Java: on IdentityKeyPair and IdentityKey, respectively
- Swift: on IdentityKeyPair and IdentityKey, respectively
- Node: on IdentityKeyPair and PublicKey; Node doesn't have a separate
IdentityKey API
For convenience, exposes IdentityKeyPair.generate() in Java and Node
as well. (This API already existed in Swift.)
Fingerprint checks are done with a boolean-returning method; the error
is never thrown. Android and iOS aren't using the exception / error
case either.
Previously this was defined in the app layers, because zkgroup's
original codegen didn't support custom exception types. However, we
can now move it to a common implementation in Rust.
This is a pretty mechanical translation *except* for
- changes to the ByteArray class to account for libsignal-client
supporting fixed-size arrays in the bridge layer
- introducing the Randomness wrapper struct to manage the 32 bytes of
randomness zkgroup uses
- removing ZkGroupException in favor of SignalError, which has a new
'verificationFailed' case
Everything else replaces zkgroup's generated "ffiapi" entry points with
libsignal-client's bridge_fn entry points.
These APIs are designed to match the generated "simpleapi" entry
points in the original zkgroup repository, to make it easier to adapt
the existing Java, Swift, and TypeScript code to libsignal-client.
The cbindgen-generated signal_ffi.h now includes constants, so that
the fixed-size arrays used to serialize zkgroup types can use named
constants in Rust. This meant filtering out some constants that were
getting picked up but that should not be included.
Note that this commit makes references to Java exception types that
will be added in a later commit.
Unlike bridge_get or bridge_get_bytearray, bridge_deserialize doesn't
do any complicated transformation of the return value to accept
optional or non-optional, failable and non-failable results alike. At
the same time, its syntax has been subtly different from the other
bridge_fn macros, dating from when we were first setting up this
library. Since the extra parameters to rename or disable a particular
bridge's entry point were rarely used, this commit removes them and
replaces those use sites with spelled-out bridge_fns. This in turn
allows removing the custom per-bridge implementations of
bridge_deserialize in favor of a bridge_fn-based implementation like
bridge_get already has.
Previously, we had HKDF-for-session-version-3, which matches RFC 5869,
and HKDF-for-session-version-2, which produced slightly different
results. However, nothing in the current versions of Signal uses
anything but the RFC-compliant version. Therefore, this commit removes
support for version 2 and deprecates the entry points that take a
version:
- Java: The HKDFv3 class is deprecated in favor of static methods on
the HKDF class.
- Swift: The hkdf function that takes a 'version' parameter is
deprecated in favor of a new overload that does not.
- TypeScript: The HKDF class is deprecated in favor of a top-level
hkdf function.
- Rust: The libsignal-protocol implementation of HKDF has been removed
entirely in favor of the hkdf crate.
There are no significant benchmark deltas from this change, and a
minimal code size increase that's the cost for removing our own
implementation of HKDF. The deprecations can be removed as a later
breaking change.
The Swift version of the NativeHandleGuard change for Java. This one's
mostly being proactive, since the Swift compiler will not optimize
across modules at this time without explicitly marking code as
inlinable, but it's possible that an operation that creates and
destroys an object entirely within the SignalClient module could have
the deinitialization of the Swift wrapper happen before the Rust
object pointer's final use. withExtendedLifetime protects against
this, and withNativeHandle wraps that up to access the native object
pointer at the same time.
Previously some classes used ClonableHandleOwner and some managed
native handles manually. Breaking out the non-cloning parts of
ClonableHandleOwner into a superclass and consistently using inherited
initializers allows us to handle wrappers of Rust objects more
uniformly.
- cargo update
- But stay on our fork of curve25519-dalek (pinned at 3.0.0)
- Update x25519-dalek from 1.0 to 1.1 (instead of 1.2) to stay
compatible with curve25519-dalek
- Update cpufeatures to 2.1 to match our dependencies
- Note that updating picky* resulted in more duplicate crates (rand*)
- Pin num-bigint-dig to a build that supports Cargo's -Zbuild-std,
because xargo + autocfg has stopped working with the new toolchain
- Remove xargo in favor of -Zbuild-std
xargo does print a reasonably nice error for this, but it doesn't
mention which toolchain to install rust-src for. Check ourselves so
that CocoaPods clients don't have to do something weird like "cd into
the package and run rustup there".
This dedicated error is thrown when a recipient has a registration ID
that's out of the range used by Signal [0, 0x3FFF]. These IDs cannot
be encoded in the sealed sender v2 format and are not supported, even
though they don't cause any problems for 1:1 messages.
The signal-crypto struct Aes256Ctr32 is still useful because we use a
different nonce size than RustCrypto's "full block", and we provide a
convenience constructor to specify an initial counter value.
This allows a device to know whether it's the one that sent a bad
message, and take action accordingly.
We could have a slightly more typesafe API here by using
ProtocolAddress and extracting the device ID, but that doesn't match
up with getting the device ID out of a sealed sender certificate.
- Default: sender will not resend; an error should be shown
immediately
- Resendable: sender will try to resend; delay any error UI if
possible
- Implicit: don't show any error UI at all; this is something sent
implicitly like a typing message or a receipt
This checks if there is an active sender state using the given ratchet
key, for use with decryption error messages. In this case, the app may
choose to archive the current session, or take even stronger actions
such as fetching new prekeys for the recipient.
The app-visible change is that sealedSenderMultiRecipientEncrypt now
takes a SessionStore as well. Sessions will be looked up in bulk using
a new SessionStore API, 'loadExistingSessions' or
'getExistingSessions`. The registration ID is then loaded from each
session and included in the resulting SSv2 payload.
The implementation is a bit of a divergence from some other APIs in
libsignal-client in that the "look up in bulk" step is performed in
the Java, Swift, or TypeScript layer, with the resulting sessions
passed down to Rust. Why? Because otherwise we'd pass a list of
addresses into Rust, which would have to turn them back into a Java,
Swift, or TypeScript array to call the SessionStore method. This would
be (1) a bunch of extra work to implement, and (2) a waste of CPU when
we already /have/ a list of addresses in the correct format: the
argument to sealedSenderMultiRecipientEncrypt.
This is an example of "the boundaries between the Rust and
Java/Swift/TypeScript parts of the library don't have to be perfect;
they're internal to the overall product". In this case, we've taken
that a little further than usual: usually we try to make the
libsignal-protocol API as convenient as possible as well, but here it
had to be a bit lower-level to satisfy the needs of the app language
wrappers. (Specifically, callers need to fetch the list of
SessionRecords themselves.)
P.S. Why doesn't v1 of sealed sender include registration IDs? Because
for SSv1, libsignal-client isn't producing the entire request body to
upload to the server; it's only producing the message content that
will be decrypted by the recipient. With SSv2, the serialized message
the recipient downloads has both shared and per-recipient data in it,
which the server must assemble from the uploaded request. Because of
this, SSv2's encrypt API might as well produce the entire request.