libsignal 7.1.0
libsignal: ^7.1.0 copied to clipboard
Dart wrapper for libsignal. Signal Protocol implementation for end-to-end encryption, sealed sender, group messaging, and secure cryptographic operations.
7.1.0 - 2026-08-15 #
For Users #
✨ Highlights
-
Every Signal Protocol message type is now inspectable from Dart —
PreKeySignalMessage,SenderKeyMessage,SenderKeyDistributionMessageandPlaintextContentjoinSignalMessageandDecryptionErrorMessage. Messages could be produced and consumed but never read, which is why a group ciphertext's owndistributionIdhad to be known out-of-band and a session's first post-quantum ratchet payload was unreachable (#62) -
flutter testworks again for Flutter apps that depend on this package (#63) — on macOS and LinuxLibSignal.init()could not find the native library the build hook had just provisioned for the test runner, so a dependent package's own unit tests failed on a clean checkout while the app itself built and ran fine -
Sealed sender gains content hints, group ids and multi-recipient fan-out —
UnidentifiedSenderMessageContentmakes the sealed envelope itself addressable, so a recipient can learn whether an undecryptable message is worth a resend request without decrypting it;sealedSenderMultiRecipientEncryptWithCallbacksproduces one Sealed Sender v2 message for a whole group -
libsignal v0.101.0 — upstream's work this release is in zkgroup, zkcredential and the net/chat APIs; nothing moved in the crates this package binds
-
libsignal_frb v6.1.1 — Rust FFI bindings
Changed
-
New
PreKeySignalMessagetype (#62) —PreKeySignalMessage.deserialize(data: bytes)plusserialize(),messageVersion(),registrationId(),preKeyId(),signedPreKeyId(),kyberPreKeyId(),kyberCiphertext(),baseKey(),identityKey(),message()andcloneMessage().message()returns the wrappedSignalMessage, which is what makes a session's very first post-quantum ratchet payload readable:PreKeySignalMessage.deserialize(data: ct).message().pqRatchet(). Purely additive — nothing in the existing surface changed, and decryption still goes throughSessionCipher, which owns the stores this type has no access to. As withSignalMessage.deserialize, parsing validates structure but does not authenticate: the inner MAC is only checked during decryption, so anything read off an un-decrypted message is attacker-controlled -
New
SenderKeyMessageandSenderKeyDistributionMessagetypes —distributionId(),chainId(),iteration(),messageVersion(), plusciphertext()andverifySignature()on the message andsigningKey()on the distribution message. This is what lets a recipient derive thedistributionIdthatGroupCipher.decryptandprocessDistributionMessagerequire, instead of having to carry it alongside the ciphertext. The distribution message's chain key is deliberately not exposed: it is secret key material, there is no constructor to pair it with, and an accessor would only add a way to leak it -
New
PlaintextContenttype —PlaintextContent.fromDecryptionErrorMessage(...)builds the envelope aDecryptionErrorMessagetravels in, which previously could be parsed from an incoming message but not produced, leaving the retry-receipt flow half-implemented. NoteDecryptionErrorMessage.extractFromSerializedContenttakesbody(), notserialize()— it rejects the leading identifier byte -
New
UnidentifiedSenderMessageContenttype andContentHintenum — build the inner payload of a sealed sender message yourself to set a content hint (none/resendable/implicit, with unknown values passed through) and a group id, then seal it withsealedSenderEncryptFromUsmcWithCallbacks.sealedSenderDecryptToUsmcWithCallbacksgoes the other way: it returns the envelope without decrypting the message inside, which is how a client reads the hint for a message it cannot decrypt. It requirestrustRoot,timestamp,localName,localDeviceIdandgetIdentity, and runs all three of the checks that stand betweenSealedSenderCipher.decryptand its plaintext — see Security below. An empty group id is omitted from the serialized form, so it reads back as absent after a round trip -
Sealed Sender v2 multi-recipient encryption —
sealedSenderMultiRecipientEncryptWithCallbacksencrypts oneUnidentifiedSenderMessageContentfor many destinations at once, producing the single SentMessage blob a server fans out;sealedSenderV2ParseSentMessagereads that blob so it can be split per recipient, returning each recipient's service id, devices with their registration ids, and the offsets of that recipient's message within the blob.SealedSenderV2SentMessage.receivedMessageFor(recipient:, data:)assembles one recipient's ready-to-deliver message from them — one at a time, so the shared body is never copied per recipient. Excluded recipients are listed with no payload. Sessions are read but never advanced, so nothing needs storing. Two constraints the single-recipient path does not have: destination address names must be service ids (a bare UUID orPNI:<uuid>), and destination registration ids must fit in 14 bits (0..=16383). Identities are resolved throughgetIdentityand an unknown one is refused rather than trusted on first use — per contiguous run of destinations sharing an address name, from the run's first destination, which is what Sealed Sender v2's per-account key material requires. Group each account's devices together in the list and the behaviour is uniform; SECURITY.md has the table -
libsignal v0.100.0 → v0.101.0 — nothing upstream changed in the surface this package binds. Across the whole range, the only file touched in
libsignal-protocol,libsignal-coreandsignal-cryptoisrust/core/src/version.rs, the version constant itself;make codegenagrees, producing byte-identical bindings. Upstream's work went to zkgroup (GenericServerSecretParamsandGenericServerPublicParamsdropserde::Deserializein favour ofTryFrom<&[u8]>, and call-link credentials now record which params version issued them), zkcredential, and the typed net/chat APIs (submitCallQualitySurvey(),AuthUsernamesService.confirmUsername()) — none of which this package exposes. The BoringSSL bump that comes with it (boring-rs v5.2.0) does not reach the binary either:boringis not in this crate's dependency graph at all. The remaining lockfile movement is patch-level transitive bumps
Security
-
sealedSenderDecryptToUsmcnow checks identity trust, not just the certificate chain — it validated the sender certificate against the trust root but never compared the identity key that certificate carries against the one stored for that sender, while its doc comment claimed parity withSealedSenderCipher.decrypt. A certificate that chains to the trust root but binds a different identity key was unsealed and reported as that sender, silently — where the decrypt path refuses the identical message withuntrusted identity. The everyday form of this needs no attacker at all: a peer who re-registers gets a valid certificate carrying a new identity key, which is a safety-number change, and a caller using the envelope to decide who to send a resend request to would never have seen it. The function now takes agetIdentitycallback and applies the same rule as everywhere else in the bridge — nothing stored is first use and is accepted, a stored identity must match — raising the sameuntrusted identityerror. Order is enforced too: the certificate chain is checked before the store is consulted, so an unvalidated (attacker-chosen) sender name can never drive a store lookup. Action required: passgetIdentity— the same callback you already givesealedSenderDecryptWithCallbacks. This changes the signature announced in #62; nothing on pub.dev shipped with the old one -
sealedSenderV2ParseSentMessageno longer amplifies its input — it built each recipient's message as its own byte array, and since every recipient's message ends with the same shared body, output grew as roughlyrecipients × message size: a 570 KB input measured at 1 GB of output, an amplification that rises with the square of the input. It now returnskeyMaterialStart/keyMaterialEndper recipient plus onesharedBytesOffset, which isO(recipients)whatever the body size, andSealedSenderV2SentMessage.receivedMessageForbuilds a single recipient's message on demand. This is also how libsignal expects a fan-out server to work —range_for_recipient_key_materialandoffset_of_shared_bytesexist for it. A message larger thanu32::MAXis now rejected rather than having its offsets truncated. Action required: replacerecipient.receivedMessagewithparsed.receivedMessageFor(recipient: recipient, data: theSameBytes) -
Sealed sender now detects a self-send, on both paths — upstream's
sealed_sender_decryptrefuses a message whose sender certificate names the receiving device, so a server that reflects your own sealed message back at you cannot have you process it as incoming. This package rebuilds that function out of its parts rather than calling it, and the check was lost on the way:sealedSenderDecryptWithCallbacksunsealed and decrypted such a message, andsealedSenderDecryptToUsmcWithCallbacksunsealed it and reported you as the sender — enough for a caller to aim a resend request at itself. Both now raiseself send of a sealed sender message, before any store is touched. The check compares the service id and the device id, so another device of your own account is unaffected. Action required:sealedSenderDecryptToUsmcWithCallbackstakeslocalNameandlocalDeviceId, the same twosealedSenderDecryptWithCallbacksalready took.sealedSenderDecryptWithCallbacksis unchanged in shape -
Secrets now survive a panicking store callback — a Dart store callback that throws unwinds the Rust worker thread (Flutter Rust Bridge declares these callbacks non-failable), and every secret in the sealed-sender path was cleared by a
zeroize()written after the work, which that unwind skips. A productionIdentityKeyStore.getIdentityover a locked database was enough to leave an identity key pair — and, for a multi-recipient send, every destination'sSessionRecord— in freed memory. Clearing is now tied toDrop(Zeroizing, and a guard around the destination list), so it happens on return, on error and on unwind alike. TheSessionRecordloaded during a sealed-sender decrypt, which was never zeroized at all, is covered too -
SealedSenderV2SentMessage.receivedMessageForrejects a buffer that is not the parsed one — it documented that it throws whendatadoes not match the parsed message, but only checked that the offsets fit. A buffer merely longer than the parsed blob was accepted and the shared run silently extended, so the delivered message grew a tail the blob never had. The parse result now carriesparsedLengthand any other length is refused. A different buffer of the same length still cannot be told apart — the doc now says that outright rather than implying otherwise.datais alsoList<int>now, matching every other byte parameter in the generated API
Fixed
-
LibSignal.init()now works underflutter test(#63) — the build hook registers the native library as aCodeAsset, but apackage:asset id is not a path: it cannot bedlopened, and Dart offers no way to ask for a registered asset's file location, so the library has to be found on disk. Only thedart run/dart testand AOT-bundle locations were probed.flutter testinstalls the very same hooked library underbuild/native_assets/<os>/and never creates.dart_tool/lib/, so on macOS and Linux the unit tests of every Flutter package depending onlibsignalfailed inLibSignal.init()on a clean tree, while the app itself built and ran fine. Windows resolved it by accident: flutter_tools prepends that directory to the test runner'sPATH, which is where Windows looks for a DLL. A leftover.dart_tool/lib/from a previousdart testwas what made it look intermittent. That directory is now probed too — last, after the AOT bundle, so a library that happens to sit in the working directory can never shadow the one a compiled application shipped with. Workaround on older versions: pass the path explicitly, e.g.LibSignal.init(libraryPath: 'build/native_assets/macos/liblibsignal_frb.dylib')(.../linux/liblibsignal_frb.soon Linux) -
A sender key distribution message processed under the wrong distribution id is now refused instead of silently dropped —
GroupCipher.processDistributionMessagetakes the distribution id from the caller, but aSenderKeyDistributionMessagealso carries one, and libsignal stores the new sender-key state under the id inside the message. When the two disagreed the state was written to a key the wrapper never reads back. On first contact that surfaced as an error, but when a record already existed under the caller's id the read-back returned that stale record, so the call succeeded while discarding the distribution message — the group's later messages then failed to decrypt with a misleading "Process a distribution message first." The ids are now compared up front and a mismatch throwsDistribution ID mismatch: message carries <x>, caller passed <y>. The matching-id path is unchanged.GroupCipher.decryptwas already fail-closed on the same mismatch and is untouched
For Contributors #
Added
-
Native-asset probe-order coverage —
test/platform/native_asset_search_paths_test.dartpins that theflutter testinstall directory is in the probe list, spelled as a per-host literal rather than by recomputing the implementation's ownPlatform.operatingSystemexpression, and that it stays behind the AOT bundle. Nothing inmake testcould have caught #63 —dart testalways resolves through.dart_tool/lib/, so the first probe wins there. The only end-to-end guard would be aflutter testleg overexample/, which does not exist yet -
PreKeySignalMessagetest coverage —test/protocol/prekey_signal_message_test.dartcovers the serialize round-trip, every accessor against the keys the session was actually built from, that inspecting a message does not consume it, and that malformed input (including a bareSignalMessage) is rejected -
Distribution-id mismatch coverage —
test/groups/distribution_id_mismatch_test.dartpins the refusal in both branches (with and without an existing record) and that two independent groups still round-trip -
Coverage for the new message and sealed-sender types —
test/groups/sender_key_message_inspection_test.dart,test/protocol/plaintext_content_test.dartandtest/sealed_sender/usmc_and_multi_recipient_test.dart, including a full multi-recipient round trip where two recipients each unseal their own message, and the refusal of an untrusted destination -
Differential tests for the Sealed Sender v2 fan-out —
rust/src/ssv2_equivalence_tests.rs(run bymake rust-test, and by CI) puts a corpus of crafted SentMessages plus a real multi-recipient message through both this package's parser and libsignal's, and asserts the message Dart reassembles from the returned offsets is byte-identical toreceived_message_parts_for_recipient. Moving that assembly out of Rust is the one place in this release where logic was rewritten rather than added, and nothing else pins it: an upstream change to the SentMessage layout would otherwise surface as multi-recipient messages quietly failing to decrypt. The same file sweeps every truncation and a few thousand byte mutations for panics, and pins that its own comparison can fail -
Identity-trust regression coverage for the sealed-sender envelope —
test/sealed_sender/decrypt_to_usmc_identity_trust_test.dartrunssealedSenderDecryptToUsmcandSealedSenderCipher.decryptagainst the same forged-certificate message and requires both to refuse it, pins trust-on-first-use for an unknown sender, and asserts thegetIdentitycallback is never reached when the certificate chain fails. Its absence is what let the gap ship.usmc_and_multi_recipient_test.dartgains the fan-out reconstruction properties and a case pinning that an unknown destination identity is refused per contiguous run rather than per device -
SPQR progress regression test —
test/protocol/spqr_ratchet_progress_test.dartruns a 200-round-trip alternating conversation and decodes the epoch and payload type out of eachSignalMessage.pqRatchet()frame instead of measuring its length. Every chunk-bearing SPQR frame is the same ~37 bytes (the encoder chunks all ML-KEM material at 32 bytes), so length says nothing about progress; the epoch does. The test asserts both sides pass epoch 1 — which requires a full ML-KEM encapsulation to have completed across ~400 store round-trips per side — and that the responder answers withCt1on exactly its third send, i.e. as soon as the third header chunk has arrived, pinning that the PreKey decrypt path applies and persists its inbound SPQR chunk — chunk 0 of that header is read straight off the PreKey message through the newPreKeySignalMessage.message(). A second case pins the responder's 4-byteNoneframes while the header is still incomplete as expected behaviour (reported as #62)
Changed
TestPartymoved totest/test_helpers/test_party.dart— it lived insidesession_cipher_test.dartand was already being imported across test files; it is now a proper helper library alongsidesession_helpers.dart
7.0.2 - 2026-08-08 #
For Users #
✨ Highlights
- libsignal v0.100.0 — dependency update only: the single change reaching the crates this package binds removes a helper this library never called, and the FFI surface regenerates byte-for-byte identical
- libsignal_frb v6.0.2 — Rust FFI bindings
Changed
- libsignal native library → v0.100.0 (compare)
- The range covers two upstream releases. v0.99.4 — upstream's own summary is "SVRB: 2026Q1 to previous", "SGX: Enforce TCB number in evidence" and "Backups: Validate the new
blockedAtTimestampfield on Contact and Group" — lands entirely inrust/net,rust/attestandrust/message-backup, alongside aLogSafeDisplayforsocks::Protocoland the Java/Kotlin binding generators. None of that is exposed by this library, and in the three crates this package binds (libsignal-protocol,signal-crypto,libsignal-core) its only diff is theVERSIONconstant - v0.100.0 is the minor bump, and the one release in range that touches a bound crate. Upstream summarises it as "SPQR: Remove requirePqRatio argument for sessions, instead requiring for all sessions". Concretely,
should_use_nonpq_session()is deleted fromlibsignal-protocolalong with its re-export and its test — the helper that decided, from a server-supplied ratio, which non-post-quantum sessions to keep and which to archive during the post-quantum ratchet rollout — and upstream's ownSessionRecord_HasUsableSenderChainbridge drops the matchingrequirePqRatioargument, so it now always demandsNotStale | EstablishedWithPqxdh | Spqr - The removal does not reach this package. It never called or exposed
should_use_nonpq_session: choosing a migration ratio is an application's policy question rather than a protocol binding's, andSessionRecord.hasUsableSenderChain()here is this package's own FRB binding, which never carried the argument upstream has now dropped. The release build is clean andmake codegenreproduceslib/src/rust/byte-for-byte, so the FFI surface is unchanged and the binding's signature is the same on both sides - Also in range but out of reach:
UnauthBackupsService.listBackupMedia, a new typed API in therust/netchat layer this package does not bind, and a zkgroup fix that stops invalid curve points being treated as candidate profile keys —zkgroupis not in this package's dependency graph at all - Upstream prepared a v0.99.5 that was never tagged, which is why two releases span three version numbers
- Both upstream GitHub releases carry an empty body; the summaries quoted above come from upstream's in-repo
RELEASE_NOTES.md, and the per-crate analysis is derived from the commit range - Transitively, the shipped binary picks up
libsignal-debug0.99.3 → 0.100.0,zerocopy0.8.55 → 0.8.56, anddata-encoding2.11.0 → 2.11.1 with itsdata-encoding-macro0.1.20 → 0.1.21 wrapper.zerocopy-derive,data-encoding-macro-internalanddelegate-attrmove as well but are proc-macros, andaho-corasick1.1.4 → 1.1.5 andregex-automata0.4.16 → 0.4.18 enter the graph only throughprost-build, a build-dependency oflibsignal-protocolandspqr— so none of those five reach the binary.THIRD_PARTY_NOTICES.txtis regenerated to match
- The range covers two upstream releases. v0.99.4 — upstream's own summary is "SVRB: 2026Q1 to previous", "SGX: Enforce TCB number in evidence" and "Backups: Validate the new
7.0.1 - 2026-08-03 #
For Users #
✨ Highlights
- libsignal v0.99.3 — dependency update only: nothing in the libsignal crates this package links changed beyond added tests and version strings, and the FFI surface regenerates byte-for-byte identical
- libsignal_frb v6.0.1 — Rust FFI bindings
Changed
-
libsignal native library → v0.99.3 (compare)
- Upstream work across v0.99.2 and v0.99.3 targets the chat/backup transport, key transparency, the SVR2 enclaves and their attestation, a PNI-less zkgroup
AuthCredentialAPI, and the Node/Java/TypeScript bindings — none of which this library exposes - Of the crates from that repository which reach the binary — the three this package binds (
libsignal-protocol,signal-crypto,libsignal-core) plus the transitivelibsignal-debug— the only source change in either release is two added#[test]functions covering HPKE invalid inputs insignal-crypto; everything else is theVERSIONconstant. The FRB bindings regenerate byte-for-byte identical, so the FFI surface is unchanged - Neither upstream release published release notes, so this entry is derived from the commit range rather than from a changelog
- Transitively, the shipped binary picks up
aes0.9.1 → 0.9.2 andhybrid-array0.4.13 → 0.4.14 (the RustCrypto array crateaesis built on).cc,clang-sys,displaydoc,eitherandtoml_parseralso move, but reach this crate only as build-dependencies or through proc-macro subtrees, so none of them ship.THIRD_PARTY_NOTICES.txtis regenerated to match
- Upstream work across v0.99.2 and v0.99.3 targets the chat/backup transport, key transparency, the SVR2 enclaves and their attestation, a PNI-less zkgroup
-
Encryption of store contents at rest is documented — every record a store persists serializes with its private key material included, and the library holds no key to encrypt it with: it is a pure Dart package with no platform-channel access, so it cannot reach Keychain, Android Keystore, DPAPI or libsecret, and on the web no key source exists that does not require a passphrase each session. A new
SECURITY.mdsection gives the sealed-store pattern on the already-publicAes256GcmSiv+hkdfDerive— KEK installed once as an opaque handle, AAD bound to the slot being read, nonce rules and why GCM-SIV rather than GCM, a format version byte — plus a per-platform table of where the KEK comes from and an explicit statement that this protects against an attacker who reads your storage, not one executing code in your process
For Contributors #
Changed
-
.fvmrcno longer drifts on everymake codegen—flutter_rust_bridge_codegenshells out tofvm install, andfvm installrewrites.fvmrcand.vscode/settings.jsonwhenever they are not already byte-identical to what it would emit. The committed files were not: fvm orders the keysflutter, flavors, runPubGetOnSdkChanges, updateVscodeSettings, updateGitIgnoreand writes no trailing newline, and it rewritesdart.flutterSdkPathto the version-pinned.fvm/versions/<v>. So every codegen run left two modified files behind, and the nightly libsignal-update workflow — which runs codegen and thencreate-pull-requestwithoutadd-paths— swept them into its PR commits..fvmrcis now committed in fvm's own serialization withupdateVscodeSettings: false, which makesfvm installa byte-level no-op on both files; verified by runningmake codegenand comparing checksums. fvm writes the file with Dart'sJsonEncoder.withIndent(' ')+writeAsStringSync, which emits LF and no trailing newline on every platform, so.fvmrcis also marked-textin.gitattributes— otherwise a Windows checkout under the defaultcore.autocrlf=truegets CRLF, never matches, and is silently rewritten on every install..vscode/settings.jsondeliberately keeps.fvm/flutter_sdkrather than fvm 4's version-pinned path: the symlink is still created by fvm 4, so it works on fvm 2, 3 and 4 alike, while.fvm/versions/3.38.4breaks for anyone on fvm 2.x and needs editing on every Flutter bump. Leaving the file to fvm was the worse option in any case — where fvm has no privileged access (Windows without Developer Mode, where it also creates neither symlink) it writes an absolute, machine-local SDK path into this committed file. The one cost is a[WARN] You are using VSCode, but fvm is not managing VSCode settingsline on each install; do not "fix" it by removing the setting -
The pre-commit hook reports a missing toolchain as a missing toolchain — any failure of step 1 was announced as
Formatting check failed. Run 'make format', so a hook run from an IDE or GUI git client — which inherits a minimal PATH and cannot findfvm,makeorcargo— sent you looking at your code instead of your PATH. The hook now appends the usual install locations before the first check — appended rather than prepended so a tool deliberately placed earlier in PATH keeps winning, and covering both the Unix (~/.pub-cache/bin) and the Windows/Git-Bash (%LOCALAPPDATA%\Pub\Cache\bin) pub-cache layouts, honouringPUB_CACHE/CARGO_HOME, and adding only directories that exist. It then checksmake,fvmandcargoare present up front, and distinguishes exit 127 from a genuine check failure so a broken environment is never reported as a code problem. Both the old and new hooks areshellcheckclean -
Discard FVM config changesinsetup-fvmis documented as a guard, not a fix — a step in a composite action can only clean up after that action, whilefvm installalso runs later in the job from insidemake codegen, so its position was never the defect. Comment only; the config change above is the actual fix -
make setup-repo-protectionsnow turns on automatic head-branch deletion — the script applied rulesets and thenative-buildenvironment but never touched repo settings, sodelete_branch_on_mergesat at GitHub's default of off and every merged branch stayed forever; 42update-libsignal-*branches had accumulated since v0.86.10 (deleted, and each is still reachable through its pull request'srefs/pull/<n>/head).delete-branch: trueonpeter-evans/create-pull-requestdoes not cover this — it only removes branches the action itself closes as obsolete. The script now also sendsPATCH repos/<slug>withdelete_branch_on_merge=true, warning rather than failing when it cannot. Note that GitHub performs the deletion as whoever merged the pull request, so theDelete branchesruleset confines it to that ruleset's bypass actors (repository admins here); for anyone else it quietly does nothing, which leaves the branch exactly where the setting being off would have left it -
A mistyped signing passphrase no longer aborts a release, and an interrupted one is resumed by re-running the same command —
gitsigns a commit or a tag by shelling out tossh-keygen -Y sign, which reads the passphrase exactly once and callsfatal()on a failed load rather than re-prompting. One typo therefore killed the release wherever it happened, and the position that hurts is between the commit and the tag, because that state blocks its own recovery: the version bump is committed, no tag exists, and re-running trips the "must be greater than the current version" precondition. Both stages now route every signing and push step throughrunInheritRetry, which prints the failure and runs the step again, so the prompt simply comes back the waysshandsudobehave — Ctrl-C is the way out, which works becauseinheritStdiodelivers the interrupt to the whole foreground process group. The loop is uncapped (an attempt limit would reinstate the failure it exists to prevent), a non-interactive stdin throws on the first failure so CI behaviour is unchanged — tested viastdin.echoMode, deliberately nothasTerminal, which calls a run redirected from/dev/nullinteractive — and from the third consecutive failure it paces itself at two seconds so a step failing in milliseconds cannot scroll past faster than it can be read.alreadyDoneis consulted after a failure so a step whose effect already landed reports success instead of being attempted twice, andbeforeRetryre-stages the release files before each commit retry, because our own pre-commit hook runsmake rust-check, whosecargo checkrewritesrust/Cargo.lockwhen the crate version moved. Separately, a Ctrl-C or a closed terminal is now recognised:isResumableReleaserequires all of a clean tree, the version file already reading exactly the requested version, andHEAD's subject equal to the exact subject the release writes (held in onecommitSubjectvariable passed both togit commit -mand to the predicate, so the two cannot drift apart), and a leftover tag is accepted only when it is this release's tag and points atHEAD. Interrupting before the commit is the one case nothing can report at the time, so the "working tree is not clean" error now usesonlyTheseFilesDirtyto name the singlegit restorethat discards the release's own edits — declining to suggest one for an untracked path or a rename, where the command would not work or would take something else with it. Covered by a newtest/scripts/release_common_test.dart(13 cases over both predicates); the retry loop's own I/O is driven by a terminal by construction and was verified against a pty upstream instead -
copier template adopted: v4.1.0 → v4.2.0 — three of the five commits in this range are the template's adoption of fixes made here first (the
.fvmrc/.vscode/settings.jsondrift, the pre-commit hook's PATH handling, anddelete_branch_on_merge), and all three came back byte-identical, socopier updateleft those files untouched. The template's fourth fix — that itspre-commithook shipped mode 644 and therefore never ran in a generated project — never applied here: this repo's hook has been 755 since it was added. What actually lands is the release-script work above, plus two documentation carriers for a decision this repo already made:.vscode/settings.jsongains the header explaining why it is committed and whyfvm install's "removeupdateVscodeSettings: false" warning must not be acted on, andCONTRIBUTING.mdgains an Editor Setup (FVM) section saying the same for contributors, including the note that Windows needs Developer Mode before the firstfvm installfor the.fvm/flutter_sdksymlinkdart.flutterSdkPathpoints at. Adopting the release-script change now is deliberate: no release is in flight, so unlike the v3.0.2 adoption it cannot alter the behaviour of a run already under way -
copier template adopted: v4.2.0 → v4.3.0 — the template now applies its own updates instead of only announcing them:
make update-template(scripts/update_template.dart,scripts/src/update_template.dart, and atest/scripts/update_template_test.dartcovering the unmerged-path parser and the CHANGELOG insertion) runscopier update, reports what it could not merge, and files the adoption entry; the scheduled workflow runs it and opens a pull request carrying the result, the way the libsignal update workflow already does. It reports two failure modes separately because both are quiet: conflicts leave both sides in the file and make the pull request a draft — nothing else catches them, sinceformat-check,rust-checkandanalyzeread only Dart and Rust while copier's conflicts land in Markdown — and.copier-answers.ymlfailing to move_commitfails the job after the pull request exists, because that state merges as an un-updated project and re-opens the same pull request forever. Copier is pinned (copier==9.11.1,jinja2-strcase==0.0.2) for the reason the actions are pinned by SHA: this runs unattended, and a copier release that changed how it merges would arrive as a conflict-shaped diff rather than a clean failure. The gates the pre-commit hook runs are executed and reported in the pull request body but never enforced — a template update that breaks a gate is precisely the one a human most needs to seeAlso fixed: the
git restorehint added in v4.2.0 never fired. The release scripts readgit status --porcelainthroughgit(), which trims its output; the two status columns are positional, so an unstaged modification is' M path', and trimming ate the leading space of the first line and shifted that path by one character.onlyTheseFilesDirtythen matched nothing and rejected the whole status, so every interrupted release got the generic "working tree is not clean" instead — in exactly the case the hint was written for, because a release edits its files without staging them. Both scripts now read the status through agitStatus()that strips only trailing newlines, and a test pins the two shapes against each other so a future trim cannot pass unnoticed. This is why the update was taken before the release rather than after itThe fourth commit in the range releases the template repository itself and touches nothing under
template/, so it does not reach here.copier updateproduced no conflicts and no.rejfiles, and_commitlanded on v4.3.0 unaided; none of this repository's standing divergences (fuzz.yml,SECURITY.md,CLAUDE.md's two-stage Release Flow, the rulesets' populated bypass actor,scripts/src/update_changelog.dart's project-specific prompt) were in range —CLAUDE.mdtook a single new line in its command list
7.0.0 - 2026-07-30 #
For Users #
✨ Highlights
- Kyber pre-keys are marked used on every decryption path, with libsignal's full argument list — (breaking) closes a gap where
SealedSenderCipher.decryptconsumed a Kyber pre-key without ever telling the store, and widensKyberPreKeyStore.markKyberPreKeyUsedto the three arguments libsignal's own store trait receives, so last-resort anti-replay becomes implementable - Pre-key consumption follows libsignal instead of guessing at it — a redelivered pre-key message no longer re-consumes the one-time keys libsignal deliberately left alone, and the session is now persisted after those writes, so a crash between the two cannot leave a one-time pre-key usable forever
- Store durability, write ordering and rollback are a documented contract — every store interface and cipher, plus a new
SECURITY.mdsection, now state what your implementation has to guarantee. This corrects rather than extends the previous advice: a lock inside the store leavesload → ratchet → storeunprotected, so two concurrentencryptcalls for one address derive the same message key THIRD_PARTY_NOTICES.txtships with the package — the prebuilt native library is statically linked against its Rust dependency tree, and those licences require the notices to travel with a binary, including an application that embeds it. Signal's own AGPL-3.0-only crates are named there alongside the permissive majority- libsignal v0.99.1 — unchanged this release
- libsignal_frb v6.0.0 — Rust FFI bindings
Changed (Breaking)
-
KyberPreKeyStore.markKyberPreKeyUsednow takes the signed pre-key ID and the sender's base key — the signature changes frommarkKyberPreKeyUsed(int kyberPreKeyId)tomarkKyberPreKeyUsed(int kyberPreKeyId, int signedPreKeyId, PublicKey baseKey), mirroring libsignal'sKyberPreKeyStore::mark_kyber_pre_key_used. Previously only the Kyber ID reached Dart, so the last-resort check that trait documents ("check whether the same combination of pre-keys was used with the given base key before") was impossible for a consumer to implement — the data simply never arrived. Action required: update yourKyberPreKeyStoreimplementation to the new signature. Retiring a one-time key still only needskyberPreKeyId; for a last-resort key, record the(kyberPreKeyId, signedPreKeyId, baseKey)triple and treat a repeat as a replayed pre-key message. SeeKyberPreKeyStore.markKyberPreKeyUsedand limitation 5 inSECURITY.mdfor what a detected repeat can and cannot do -
SealedSenderDecryptResult.preKeyToRemoveremoved — only affects callers of the raw generated API (sealedSenderDecryptWithCallbacks);SealedSenderCipher.decryptis unchanged for its users. Sealed-sender decryption now takesremovePreKeyandmarkKyberPreKeyUsedcallbacks, which the bridge invokes itself in libsignal's order, rather than returning an ID for the caller to act on afterwards — matching howSessionCipher.decrypthas always worked. Action required: if you call the raw function, pass the two new callbacks and delete your post-callremovePreKeyhandling
Changed
- The package ships
THIRD_PARTY_NOTICES.txt— the prebuilt native library is statically linked against its Rust dependency tree, and those licences require their notices to travel with a binary distribution, including an application that embeds the library. Flutter'sLicenseRegistrydoes not cover them: it aggregatesLICENSEfiles of pub packages, and Rust crates are not pub packages. The file sits at the package root and is generated from the resolved dependency graph with no platform filtering at all, so the same commit yields the same file on any machine — build edges are included because that is how vendored native code reaches the binary — and CI verifies it stays in sync withCargo.lock. It is not an inventory of permissive licences: Signal's own crates in that graph (libsignal-protocol,libsignal-core,signal-cryptoand their siblings) are AGPL-3.0-only, and they are named alongside the MIT / Apache-2.0 / BSD / ISC majority, with the README's new Third-party notices section pointing at LICENSE.libsignal for what that means when you redistribute a binary. Where a crate ships no licence file of its own, the canonical text of the licence it declares is supplied in its place, so the file delivers the licences rather than merely naming them. It is deliberately not declared underflutter: assets:, which would bundle it into every consuming application whether or not it is ever displayed; the README shows how to register it withLicenseRegistryfor an app that wants it at runtime
Security
-
Store durability, write ordering and rollback are now a documented contract — storage is delegated to the application, and libsignal derives message keys deterministically (the Double Ratchet has no per-message nonce guard), so a store write that is lost to a crash or rolled back by a restore makes the next send reuse a message key and IV. The contract is now stated where implementers read it: on every store interface (
SessionStore,IdentityKeyStore,PreKeyStore,SignedPreKeyStore,KyberPreKeyStore,SenderKeyStore), onSessionCipher/SessionBuilder/SealedSenderCipher/GroupCipher, and in a newSECURITY.mdsection. No behaviour change — the library already awaited every store-write callback before returning a ciphertext or plaintext (verified against the Rust bridge for every entry point); what was missing was the requirement that your callback not complete until the write is durable- Durable before release — a store write must reach stable storage before the operation's output leaves the device or is acted upon, either inside the callback (
fsync, SQLitesynchronous = FULL) or via a transaction committed before sending. Deletes and pre-key consumption (removePreKey,markKyberPreKeyUsed) count as writes - Serialize per address — corrects the previous guidance in
SECURITY.md§H, which suggested a lock inside the store: that leaves theload → ratchet → storewindow unprotected, so two concurrentencryptcalls for one address derive the same message key with no crash involved. The lock must span the whole cipher call - Rollback — at-rest encryption gives confidentiality, not rollback protection; documents the achievable mitigation (bind the store to a marker in non-backed-up storage and treat a restored copy as a session reset) plus the platform limits of
fsyncon Apple platforms and of IndexedDB durability on the web
- Durable before release — a store write must reach stable storage before the operation's output leaves the device or is acted upon, either inside the callback (
-
SealedSenderCipher.decryptnow marks the Kyber pre-key it consumed — it removed the one-time EC pre-key a pre-key message consumed but never calledmarkKyberPreKeyUsedfor the Kyber pre-key on that same path, so a store that retires marked one-time Kyber pre-keys kept serving one that sealed sender had already consumed. Sealed sender is a normal delivery path for a first message, so this was the ordinary case rather than a corner. Both decryption paths now issue the same four writes -
Pre-key consumption now reports what libsignal actually did, and
storeSessionis written last — the bridge inferredremovePreKey/markKyberPreKeyUsedfrom the fields of the incoming message, while libsignal issues them only when the pre-key message really establishes a new session. A redelivered pre-key message matching an existing session therefore re-consumed keys libsignal had deliberately left alone. The bridge now observes the calls libsignal makes against the stores it is handed. That change requires the session to be persisted after the consumption writes (it previously went first): had the order stayed, a crash between the session write andremovePreKeywould let the redelivered message match the persisted session, consume nothing, and leave a one-time pre-key usable forever. The awaited-write table inSECURITY.mddocuments the new order
For Contributors #
Added
-
CI verifies the declared MSRV —
rust-version = "1.88"inrust/Cargo.tomlis a promise to anyone building the native library from source, and nothing checked it: the first dependency or language feature to raise the real floor would have broken that build silently, with the failure landing on a contributor rather than here. A newmsrvjob reads the version out of the manifest — rather than repeating it, so the job cannot drift from the claim it checks — installs exactly that toolchain, installs protoc —spqr's prost-based build script shells out to it, so without it the job would fail on tooling rather than on the MSRV it exists to check — and runsmake rust-check. Verified locally against 1.88 before the job was added; the reusablesetup-rustaction gained atoolchaininput (defaultstable) to make it possible -
Reference durable store in
example_cli(repository only —example_cli/is not part of the published archive) —lib/stores/durable_file_stores.dartimplements all six stores on an append-only journal that flushes before each write's future completes and replays on open, truncating a torn tail — which, without per-frame checksums, it cannot tell apart from a corrupt header, a limitation the file documents. It ships anAddressLockshelper for call-site serialization.lib/demos/durable_store_demo.dartproves the round trip: it establishes a session, exchanges messages, closes the stores, reopens them from disk and continues the same conversation.DurableKyberPreKeyStoredemonstrates both halves of the Kyber contract: a one-time key is retired on its first mark (loadKyberPreKeystops serving it), while a last-resort key stays in service and every(kyberPreKeyId, signedPreKeyId, baseKey)agreement is journalled, with repeats surfaced throughreplayedAgreements. The demo's final step exercises that second half end to end — it rolls Bob's state back the way a restored backup would, replays the same ciphertext, and shows the identical agreement being marked twice -
test/protocol/kyber_pre_key_consumption_test.dart— pins the two behaviours that had no coverage: a second pre-key message arriving on the session an earlier one established consumes nothing further, andSealedSenderCipher.decryptmarks the Kyber pre-key with the same triple asSessionCipher.decrypt
Changed
-
stores-implementationandsecurity-reviewskills, plus theCONTRIBUTING.mdreview checklist, corrected — they recommended a lock inside the store, which does not coverload → ratchet → store, and are now aligned with the durability/serialization contract. Both transaction examples also note that the store's writes must be routed through the ambient transaction (sqflitedeadlocks if thedbhandle is used insidedb.transaction(...)) -
GitHub Actions bumped to their Node 24 majors — the first grouped Dependabot run moves
actions/checkout4 → 7,actions/upload-artifact4 → 7,actions/download-artifact4 → 8,actions/cache4 → 6,actions/create-github-app-token2 → 3,android-actions/setup-android3.2.2 → 4.0.1 andschneegans/dynamic-badges-action1.7.0 → 1.9.0, converging on the pins the copier template now carries. This is catching up to the runner rather than optional drift: CI was already warning that "actions/cache@v4, actions/checkout@v4" target the deprecated Node 20 and "are being forced to run on Node.js 24". Every input these workflows pass still exists on the new majors, and both SHA-pinned actions were verified against their upstream tag refs. The two behaviour changes that do land:download-artifactnow fails a run on a digest mismatch instead of only warning, andsetup-androiddropped its SDK cache (slower Android legs, same output). No workflow logic changed -
Dependabot branches excluded from the
Signing commitandDelete branchesrulesets — both target~ALLbranches, sonon_fast_forwardstopped Dependabot from force-pushing a rebase onto a movedmainanddeletionstopped it from cleaning up a merged branch: a grouped update PR could never refresh itself oncemainhad moved.refs/heads/dependabot/**/*is now in each ruleset'sref_name.exclude— the trailing/*is load-bearing, since a bare**does not cross a/and so would miss the multi-segment branch names Dependabot actually creates.mainis unaffected (it is not a Dependabot branch) and keepsrequired_signaturesfrom the same ruleset. Scoped withexcluderather than a bypass actor, which on a~ALLruleset would have exempted that actor onmaintoo -
copier template adopted: v3.0.3 → v4.1.0 — the major's single contract change is that every project generate and commit
THIRD_PARTY_NOTICES.txtbefore its next CI run, becausetest-reusable.ymlnow verifies it; that file and its generator arrive here for the first time (see For Users above). Also landing:make rust-testand a CI step that runs the crate's own unit tests;make third-party-notices/make verify-third-party-notices, withmake rust-updateregenerating the inventory so the lockfile and the notices cannot drift apart; the fuzz workflow reads its targets from the[[bin]]entries ofrust/fuzz/Cargo.tomland fans them out one job per target (fail-fast: false, per-target crash artefacts) instead of looping over a hardcoded list in a single job that stopped at the first crash — the discovery step was run againstrust/fuzz/Cargo.tomland yields exactly the six existing targets;validateUpstreamTagnames which input it rejected, since an APItag_name, a--versionargument and the pin recorded inrust/Cargo.tomlfail for different reasons;insertChangelogEntrymatches#### Changedexactly, where a prefix match previously also filed a native-library bump under#### Changed (Breaking); the build hook declares a local native build as a dependency, somake cleanno longer leavesdart testpointed at a cached asset that is gone; andcopyright_yearbecomes a stored answer, recorded as 2025 — the year of first publication — though for an AGPL-3.0 project it does not reach the renderedLICENSE, which the template only stamps for MIT and BSD. Three deviations are deliberate. The AI changelog prompt stays this project's own: the template now carries a generic version, while the one here enumerates the crates this wrapper binds and the upstream areas it does not expose, which is what keeps an upstream networking, keytrans or zkgroup change from being announced as a feature of this package. Thefreezed_annotation/freezed/build_runnerdependencies are not adopted — they exist so that a freshly generated project's first codegen succeeds against an unknown API surface, whereas this FRB surface has no data-carrying enums, andfreezed_annotationsits independencies, so every consumer would download a package nothing here imports. Andffigenstays at^20.1.1instead of returning to the template's^20.0.0. The follow-up minor, v4.1.0, landed net-zero: its whole content is this project's own notice-inventory and MSRV work (the two fixes below, plus the reproducibility pass) carried back upstream, socopier updatehad nothing left to apply beyond recording the version -
The
Signing commitruleset no longer bypasses the update GitHub App —bypass_actorsis now empty, matching the template. The app's commits are created through the API and are therefore signed by GitHub, sorequired_signaturesis satisfied without an exemption, and on a~ALLruleset a bypass actor is exempted everywhere,mainincluded — the same reasoning the Dependabot entry above applies.refs/heads/update-*is still not excluded from the ruleset: the template's policy is to widenexcludeonly on an observed failure, and a failure here is visible rather than silent, since the bot comments on the pull request it could not refresh
Fixed
- The notice inventory no longer depends on the machine that generated it —
cargo tree --target <triple>filters normal dependencies by that triple but resolves build-dependencies for the host, so the inventory recorded the build graph of whoever ran the generator rather than of the released targets. Here that isprost-build→tempfile→rustix, whose backend is host-gated:errnoon a macOS host,linux-raw-syson a Linux one. One crate swapped for the other with the crate count unchanged, so the file generated locally was rejected by the CI check on its first run — correct where it was written, wrong everywhere else, and the check could only report "the contents differ". Nor is the problem confined to build edges: proc-macro subtrees are host-compiled too, which is howwinapi— reached throughansi_terminside a proc-macro crate — stays invisible everywhere except a Windows host. No per-target query escapes this, so the crate set is now taken fromcargo tree --target all, the only query cargo offers that applies no platform filtering at all; the per-target sweep is kept because it is the one thing that fails when a declared release target stops resolving. Over-attribution is the deliberate trade: the extra entries are build tooling and platform-gated crates that a given build never links —winapihere reaches the graph only through a host-compiled proc-macro — but a notice file that lists them on every machine is worth more than a narrower one that changes with the machine, since the byte-exact CI check is only viable if the output is reproducible. Accordingly the inventory grows from 206 to 241 crates, the additions being platform-gated crates and build tooling that were always in the graph but invisible from a macOS host (linux-raw-sys,windows-sys,winapi,bindgen,clang-sys, …). Cross-checked againstcargo-about: it now reports no crate this inventory omits.--checknow also prints the first differing line and the lines unique to each side, since its failure is normally read from a CI log where bisecting a 450 KB file by hand is the only alternative
6.1.1 - 2026-07-25 #
For Users #
✨ Highlights
- libsignal v0.99.1 — internal/dependency update, no public-API impact
- libsignal_frb v5.1.2 — Rust FFI bindings
Changed
- libsignal native library → v0.99.1 (compare)
- Upstream user-facing changes target the chat/backup/registration services and logging — none of which this library exposes
- The crates we bind (
libsignal-protocol,signal-crypto,libsignal-core) saw internal refactors to track the updated RustCrypto / curve25519-dalek / spqr dependencies, with no change to behaviour or the FFI surface (FRB bindings regenerate byte-for-byte identical)
Security
- Upstream libcrux advisories resolved — v0.99.1 pulls in
libcrux-sha30.0.10 andlibcrux-secrets0.0.6, which fix RUSTSEC-2026-0207, RUSTSEC-2026-0208 (incremental/AVX2 SHAKE) and RUSTSEC-2026-0212 (aarch64 const-time swap). The interimcargo-audit/cargo-denysuppressions for these three have been removed
6.1.0 - 2026-07-21 #
For Users #
✨ Highlights
- Build provenance attestation (Sigstore, SLSA Build L2) — every native-release archive is now cryptographically attested to this repository's tag-triggered build, closing the previously documented authenticity gap (verify with
gh attestation verify) - Web: stale-WASM-after-upgrade fixed — the web build hook now refreshes
web/pkg/on a version change instead of serving the previous version's WASM, which could crash Dart-store-callback paths (processPreKeyBundle,SessionCipher, sealed sender, group messaging) after an upgrade - Smaller package & explicit minimum OS versions — the vestigial platform-plugin scaffolding is removed (smaller published archive) and the prebuilt binaries are now built against the documented macOS 10.15 / Android API 24 minimums
- libsignal v0.97.4 — internal/dependency update, no public-API impact
- libsignal_frb v5.1.1 — Rust FFI bindings
Changed
- Platform-plugin scaffolding removed from the published package — the vestigial
ios/,macos/,android/,linux/,windows/directories (podspecs, Gradle project, CMakeLists, plugin stubs) are gone. The package has never declared aflutter: plugin:section, so flutter_tools never consumed them; native delivery is (and remains) via thehook/build.dartbuild hook. No consumer action required — the published archive just gets smaller - Explicit minimum OS versions for the prebuilt binaries — CI now builds the macOS dylibs with
MACOSX_DEPLOYMENT_TARGET: '10.15'(previously rustc's per-target default, 10.12 for x86_64) and links the Android.sos against API level 24 via cargo-ndk--platform 24(previously cargo-ndk's default, 21), matching the documented platform-support table - libsignal v0.97.4 update — bump the bound native library (compare)
- Upstream changes are limited to
AuthAccountsService(registration-lock set/clear, discoverable-by-phone-number, registration-recovery-password),UnauthBackupsService.copyMedia/copyBackupMedia, SVR2 node APIs, and language-binding / bridge tooling (node/java/swift/ts) — none of which this library exposes - The only change to the crates we bind (
libsignal-protocol,signal-crypto,libsignal-core) is thelibsignal-coreversion string (rust/core/src/version.rs);make codegenproduces no binding diff - Note: These changes do not affect this library's public API
- Upstream changes are limited to
- libsignal v0.97.3 update — bump the bound native library (compare)
- Upstream changes are limited to
AuthUsernamesService.deleteUsernameHash()/deleteUsernameLink()(username services), reclassifying an established chat connection's transport errors as retryable (.ioError, Swift binding), and increasing the key-transparency clock-skew tolerance interval — none of which this library exposes - The crates we bind (
libsignal-protocol,signal-crypto,libsignal-core) are unchanged apart from version strings;make codegenproduces no binding diff - Note: These changes do not affect this library's public API
- Upstream changes are limited to
Security
- Build provenance attestation (Sigstore, SLSA Build L2) — every native-release archive is now attested with GitHub Artifact Attestations: CI signs a provenance statement proving the archive was built by this repository's tag-triggered
build-libsignal.ymlfrom a specific commit, closing the previously documented authenticity gap (the SHA256 checksums file ships in the same release as the archives). Verify withgh attestation verify <archive> --repo djx-y-z/libsignal_dart; a Sigstore bundle (libsignal_frb-<version>.sigstore.jsonl) is attached to each release for fully offline verification. See SECURITY.md → Authenticity (the build hook itself still verifies SHA256 only — attestation verification is manual)
Fixed
- Stale web WASM after a package upgrade — the web build hook (
hook/build.dart) now records the provisioned crate version inweb/pkg/.wasm-versionand re-downloads when it changes, instead of skipping whenever the two WASM files merely exist. Previously, upgrading the package kept the prior version's WASM in the consuming app'sweb/pkg/(it survivesflutter clean), so on web any FRB entry that calls Dart store callbacks —SessionBuilder.processPreKeyBundle,SessionCipher,SealedSenderCipher, group messaging — panicked with an argument-count mismatch (called Option::unwrap() on a None value) once the wire signature had changed between versions. The download cache is now version-keyed andrust/Cargo.tomlis a declared web-build dependency, both mirroring the native path (which was unaffected) - Build hook download/cache resilience — the hook (
hook/build.dart) is more robust against partial/transient failures: a download-cache entry is only reused after a.download-completemarker proves the extraction finished (an interruptedtarno longer leaves a truncated library that is reused forever), a locally builtrust/target/library is used only when it matches the target OS and architecture (previously a host build could be bundled for a cross-target, e.g. a macOS dylib into an iOS app), the web path no longer fetches checksums when a warm cache can serve the files offline, and both the checksums fetch and the binary download now retry on transient HTTP 5xx/429 instead of failing the build on a single blip
For Contributors #
Added
make release-frb+release-frb-crateskill — one-command native-crate release (stage 1): bumpsrust/Cargo.toml, stamps the CHANGELOGlibsignal_frbHighlights line, and creates a signed commit +libsignal_frb-<version>tag, pushing to trigger the native build. The commit/tag/push inherit the terminal, so the signing passphrase is entered interactively during the command. Pairs withrelease-package(stage 2)make release+ updatedrelease-packageskill — one-command Dart package release (stage 2) symmetric tomake release-frb: verifies the stage-1 native binary exists on GitHub Releases, bumpspubspec.yaml, finalizes the CHANGELOG ([Unreleased]→ dated version + a fresh[Unreleased]+ the bottom compare-link refs), validates with a publish dry-run, then signs a commit +vX.Y.Ztag and pushes to trigger the pub.dev publish. The two release commands share git/terminal helpers inscripts/src/release_common.dart- Repository-protection tooling — the branch and release-tag rulesets now live in-repo as committed JSON (
.github/rulesets/*.json, the source of truth), andmake setup-repo-protectionsapplies them to GitHub viagh(idempotent by ruleset name) and configures thenative-buildenvironment. A new Protect release tags ruleset restricts tag creation (all tags) to Admins/Maintainers — covering the release-triggeringlibsignal_frb-*/v*and any other — and the native-crate publish (build-libsignal.yml) now runs in the required-reviewernative-buildenvironment — gating tag-push andworkflow_dispatchalike, mirroring thepub.devenvironment that gates pub.dev publishing - Dependabot for GitHub Actions —
.github/dependabot.yml: weekly grouped update PRs (Monday 06:00 UTC,chore(deps)prefix) bump the pinned actions — both the commit SHA and its# vX.Y.Zcomment — across the workflows and the composite actions (adirectoriesglob covers/.github/actions/*, since/only scans.github/workflows/).dtolnay/rust-toolchainis ignored: it has no versioned releases (master-SHA pin, toolchain selected via input) and stays manually bumped
Changed
- Accept unremediable upstream libcrux crypto advisories in cargo-deny / cargo-audit — three RustSec advisories published 2026-07-17 (
RUSTSEC-2026-0207/-0208, incorrect / panicking SHAKE inlibcrux-sha30.0.8;RUSTSEC-2026-0212, incorrect aarch64 constant-time swap inlibcrux-secrets0.0.5) live in libsignal's git-pinned ML-KEM stack and are not fixable from this repo — the fix requires a libsignal release that bumpslibcrux-ml-kem(v0.97.4 still ships the old libcrux). Added torust/deny.toml[advisories].ignoreand therust-audit--ignoreflags as a tracked interim suppression so thecargo-deny/cargo-auditCI jobs pass — to be removed once a fixed libsignal release lands - Decoupled the
libsignal_frbnative release from libsignal dependency updates — automated update PRs no longer bump the crate version or build binaries; dependency updates accumulate onmain(tested from source in CI), and the native build is now triggered by pushing alibsignal_frb-<version>tag instead of by pushing tomain. The crate-version bump is now a deliberate release decision (make release-frb). See CLAUDE.md → Release Flow - AI changelog generator classifies upstream changes against the bound-crate surface — the prompt now states which crates/APIs this wrapper actually binds, so out-of-scope upstream changes (net / chat / keytrans / username services / zkgroup / …) are framed as "none of which this library exposes", and it links to a version
compareinstead of the (often incomplete) release notes - CI enforces deployment-target consistency —
test-reusable.ymlnow runsmake check-targets(Linux leg) so the build fails if the iOS / macOS / Android minimum deployment targets drift out of sync across the CI build env vars, the example Xcode projects and the README platform table. Previously the check existed (make check-targets) but was never run automatically - Deployment-target sources consolidated —
.copier-answers.ymlremains the single source of truth; with the platform scaffolding removed,make check-targetsandscripts/get_android_min_sdk.dartno longer read the podspecs/build.gradlebut verify the CI workflow (IPHONEOS_DEPLOYMENT_TARGET,MACOSX_DEPLOYMENT_TARGET, cargo-ndk--platform) instead - Upstream tag names validated before reaching the shell —
check_updates.dart/check_template_updates.dartreject a releasetag_namethat is not a plain semver-ish tag before it lands inGITHUB_OUTPUT, and the update workflows pass step outputs/inputs intorun:blocks viaenv:instead of inline${{ }}interpolation — closing a shell-injection path from upstream release names (backport of the liboqs audit) - Least-privilege
GITHUB_TOKENeverywhere —publish.ymlandbuild-libsignal.ymlnow default tocontents: readwith job-level opt-ups (id-token: writeon the pub.dev publish job,contents: writeon the release jobs); the two update-checker workflows dropcontents/pull-requests: writeentirely (all writes go through the App token) - Third-party actions pinned to commit SHAs —
dart-lang/setup-dart,peter-evans/create-pull-request,android-actions/setup-android,ilammy/msvc-dev-cmd,schneegans/dynamic-badges-action,Swatinem/rust-cache,dtolnay/rust-toolchain(toolchain now passed via thetoolchaininput since the ref no longer selects it) setup-makeverifies gnumake.exe by SHA256 — release assets are mutable, so the size check alone did not lock the Windows make binary; a hardcoded SHA256 (updated together with the version) now does- Pre-release hardening pass (audit fixes) — a review of this cycle's changes fixed, among others:
make release-frbnow syncs and stagesrust/Cargo.lockalongsiderust/Cargo.toml(the pre-commitcargo checkno longer leaves a dirty tree that blocked stage 2, and the signed tag no longer carries a stale lock);Swatinem/rust-cacheis repinned from the floatingv2tag object to the realv2.9.1commit (would have broken every Rust job when upstream re-taggedv2); the pub.dev release notes are written via--notes-fileinstead of an inline heredoc (a literalEOFline in the changelog can no longer break out into the shell);build-libsignal.ymlno longer delete-then-recreates a release (fail-loud, no silent clobber) and the release-existence probe fails closed on API errors; thefuzz.ymldispatchdurationinput is validated and passed viaenv:;make check-targetsfails closed when a checked file/pattern disappears; and--dateinmake releaseis validated. Docs corrected across README/CLAUDE/CONTRIBUTING/SECURITY/rulesets (build-hook fallback,AI_MODELS_TOKEN, two-stage publishing,.skip_*_hooksemantics, stale Cargokit/loading-order references) - Adopt copier template v3.0.0 — most of this template release (the two-stage release flow, repository rulesets, Dependabot, and the CI deployment-target check) was already backported into this repo, so the update reduced to a documentation and tooling sync:
CONTRIBUTING.mdgains the "Releasing (two stages)" and "Repository rulesets & tag protection" sections, and theMakefile.PHONYlist is reordered to match the template (no behavior change)
6.0.0 - 2026-07-14 #
For Users #
✨ Highlights
- Identity-trust enforcement (breaking) — a remote identity key that differs from the stored one is now rejected with
UntrustedIdentityon every session operation (MITM / safety-number-change detection), instead of being silently accepted - Hardened supply chain & binary — the native-binary download is now fail-closed (aborts if it can't be verified), and the wrapper crate is built with integer-overflow checks
- App store additional permission — the license now allows AGPL-compliant apps to ship through app stores with AGPL-incompatible terms (e.g. the Apple App Store); see
LICENSE.appstore - libsignal v0.97.2 — internal/dependency update, no public-API impact
- libsignal_frb v5.0.0 (internal Rust FFI crate) — breaking (major): adds a required
get_identitycallback
Changed (Breaking)
- Identity-trust is now enforced on every session operation, matching upstream libsignal's
is_trusted_identitysemantics.SessionBuilder.processPreKeyBundle,SessionCipher.encrypt/decrypt(both pre-key and regular Whisper messages), andSealedSenderCipher.encrypt/decryptnow consult yourIdentityKeyStore.getIdentityand reject a remote identity key that differs from the stored one with anUntrustedIdentityerror. Previously a substituted identity (e.g. from a malicious key-distribution server) was accepted without error. First contact is still trusted-on-first-use.- Action required: catch
UntrustedIdentity(its message containsuntrusted identity) and treat it as a safety-number change — verify with the user, then save the new identity (or clear the old one) in your store and archive the old session for that address before retrying. Requires yourIdentityKeyStore.getIdentityto be implemented correctly.
- Action required: catch
Changed
- Update libsignal native library to v0.97.2 (compare)
- Upstream changes between v0.96.4 and v0.97.2 are limited to net/registration, chat/backups gRPC, bridge/codegen tooling, and CI / language-binding (node/swift/java) updates — none of which this library exposes
- The only diffs in the crates we bind are cosmetic: a test-only import in
kem.rs, an internalTryFromrefactor instate/bundle.rs(and_then(…map…)→.zip(…), behavior identical), and thelibsignal-coreversion string - Note: These changes do not affect this library's public API
- App store additional permission (AGPL §7) — the package license now carries an explicit app-store exception (the Feeel/wger wording, see
LICENSE.appstore): GPL/AGPL-compliant applications may distribute this package in object-code form through app stores whose terms are incompatible with the AGPL (such as the Apple App Store), provided their source stays available under the AGPL through an unrestricted channel. The permission covers only this repository's code; the status of an equivalent permission for the bundled upstreamlibsignalis tracked in signalapp/libsignal#684. Requested in #44
Security
- Fail-closed native library verification — the build hook (
hook/build.dart) now aborts the build if the SHA256 checksums for a downloaded binary cannot be fetched or the archive has no entry, instead of silently proceeding unverified. An escape hatch (LIBSIGNAL_ALLOW_UNVERIFIED_DOWNLOAD=1) remains for releases with no checksums file - Hardened crate build — the wrapper's release profile enables
overflow-checks, so an integer overflow in the wrapper is a deterministic (catchable) panic rather than silent wraparound (the audited crypto dependencies are left untouched) - Secret-lifetime & zeroing caveats documented —
SECURITY.mdnow spells out that opaque secret handles (PrivateKey,KyberSecretKey,SessionRecord, …) stay resident in native memory until a non-deterministic GC finalizer runs, so security-critical code should calldispose()to bound that window (noting the extractable key types areCopy/plain-boxed and thus not zeroized on drop —dispose()shortens the exposure window, it does not wipe), and that Rust'szeroizecovers Rust memory only: secret bytes that cross the FFI boundary into a DartUint8Listlive on the un-zeroed GC heap whereSecureBytes/zeroize()are best-effort.PrivateKey.cloneKey()/KyberSecretKey.cloneKey()now carry a# Securitydoc note that each copy is an independent secret
Fixed
- Device ID truncation — the
ProtocolAddressandPreKeyBundleconstructors no longer truncate theu32device ID tou8before validating (e.g.257is no longer accepted as device1); out-of-range IDs are rejected as documented (1–127) - HKDF output bound —
hkdfDerivenow rejects an output length above the RFC 5869 maximum (255 × 32 = 8160bytes) before allocating, instead of attempting an oversized allocation - In-memory identity-store equality —
InMemoryIdentityKeyStorenow compares identity keys by value (equals()) rather than by object reference, so a re-presented key is correctly seen as unchanged (matters for production stores copied from this reference implementation) - Native-library download cache key — the build hook (
hook/build.dart) now keys its download cache by crate version and the full platform variant (e.g.ios-device-arm64vsios-simulator-arm64) rather than only OS + architecture. On Apple-silicon hosts iOS device and simulator builds shared a key, so whichever built first poisoned the cache for the other anddyldrejected the bundled library at runtime (incompatible platform: have 'iOS-simulator', need 'iOS'); a version bump could also serve a stale cached binary
For Contributors #
Added
- Fuzzing harness —
cargo-fuzztargets (rust/fuzz/) covering every byte-parsing entry point (keys, messages, records, sealed-sender certificates, crypto primitives, pre-key decryption), a seed-corpus generator, and aFuzzCI workflow (per-PR smoke run + weekly deep run). Seemake fuzz-list/make fuzz - Dependency policy —
cargo-deny(rust/deny.toml,make rust-deny, CIdenyjob) enforcing RustSec advisories, an AGPL-compatible license allow-list, and a source allow-list restricted to crates.io and the official Signal repositories - Rust linting (Clippy) —
cargo clippy --all-targets -- -D warningsnow runs in CI (the reusable test workflow, on the Linux x86_64 leg) and locally viamake rust-clippy; the hand-written wrapper is lint-clean, with the FRB-inherent lints (many-callback store signatures, complex tuple returns) annotated with justified site-local#[allow]s
Changed
- CI least-privilege — the reusable test workflow now declares
permissions: contents: read - Rust lint — hand-written Rust is compiled with
unsafe_code = "deny"(only the FRB-generated bridge is exempt) - Copier template adopted (v2.5.1) —
flutter_rust_bridge_codegenis now pinned viamake setup-frb-codegen(kept in sync with theflutter_rust_bridgedependency,2.12.0); the libsignal-update workflow installs the codegen binary (fixing a codegen step that failed with exit 127) and skips regenerating an update PR that already exists;check_updates.dartbumps the wrapper crate version mirroring the upstream SemVer delta,update_changelog.dartclassifies update severity via AI, and theupdate-libsignalskill now analyzes the full upstream diff
5.0.9 - 2026-06-27 #
For Users #
✨ Highlights
- libsignal v0.96.4 — internal improvements and updates
- libsignal_frb v4.0.9 — Rust FFI bindings
Changed
- Update libsignal native library to v0.96.4 (compare)
- Upstream changes are limited to net/registration and chat gRPC helpers, server-side SVR enclave rotation (2026Q2), FFI bridge tooling, and new typed
reserveUsernameHash()/ donation-permit client APIs — none of which this library exposes - The
libsignal-protocolandsignal-cryptocrates are unchanged;libsignal-coreonly bumps its internal version string - Note: These changes do not affect this library's public API
- Upstream changes are limited to net/registration and chat gRPC helpers, server-side SVR enclave rotation (2026Q2), FFI bridge tooling, and new typed
5.0.8 - 2026-06-24 #
For Users #
✨ Highlights
- libsignal v0.96.3 — internal improvements and updates
- libsignal_frb v4.0.8 — Rust FFI bindings
Changed
- Update libsignal native library to v0.96.3 (release notes)
- Upstream changes are limited to an internal ML-KEM parameter key type fix plus net/node/gRPC/server-side updates, none of which this library exposes
- Note: These changes do not affect this library's public API
5.0.7 - 2026-06-20 #
For Users #
✨ Highlights
- libsignal v0.96.2 — internal improvements and updates
- libsignal_frb v4.0.7 — Rust FFI bindings
Changed
- Update libsignal native library to v0.96.2 (release notes)
- Upstream changes are limited to zkgroup donation credentials (
DonationPermit), which this library does not expose - Note: These changes do not affect this library's public API
- Upstream changes are limited to zkgroup donation credentials (
5.0.6 - 2026-06-19 #
For Users #
✨ Highlights
- libsignal v0.96.1 — internal improvements and updates
- libsignal_frb v4.0.6 — Rust FFI bindings
Changed
- Update libsignal native library to v0.96.1 (release notes)
- Internal improvements and updates
- Note: These changes do not affect this library's public API
5.0.5 - 2026-06-12 #
For Users #
✨ Highlights
- libsignal v0.96.0 — internal improvements and updates
- libsignal_frb v4.0.5 — Rust FFI bindings
Changed
- Update libsignal native library to v0.96.0 (release notes)
- Internal improvements and updates
- Note: These changes do not affect this library's public API
5.0.4 - 2026-06-10 #
For Users #
✨ Highlights
- libsignal v0.95.0 — internal improvements and updates
- libsignal_frb v4.0.4 — Rust FFI bindings
Changed
- Update libsignal native library to v0.95.0 (release notes)
- Internal improvements and updates
- Note: These changes do not affect this library's public API
5.0.3 - 2026-06-04 #
For Users #
✨ Highlights
- libsignal v0.94.4 — internal improvements and updates
- libsignal_frb v4.0.3 — Rust FFI bindings
Changed
- Update libsignal native library to v0.94.4 (release notes)
- Internal improvements and updates
- Note: These changes do not affect this library's public API
5.0.2 - 2026-05-31 #
For Users #
✨ Highlights
- libsignal v0.94.3 — internal improvements and updates
- libsignal_frb v4.0.2 — Rust FFI bindings
Changed
- Update libsignal native library to v0.94.3 (compare)
- Binding/tooling improvements (JNI, Node, Swift type converters), backup validator and reflector routing updates
- Note: No changes to the libsignal-protocol crate — does not affect this library's public API
Documentation
- Document
flutter build web --wasm(dart2wasm) limitation in README — Rust returns fail withType 'JSValue' is not a subtype of type 'List<dynamic>'under dart2wasm. Upstream limitation influtter_rust_bridge(#2575), affects every FRB-based Dart package. Standardflutter build web(dart2js) target continues to work.
5.0.1 - 2026-05-19 #
For Users #
✨ Highlights
- libsignal v0.94.1 — internal improvements and updates
- libsignal_frb v4.0.1 — Rust FFI bindings
Changed
- Update libsignal native library to v0.94.1 (compare)
- Networking improvements: gRPC/H2 transport additions, reflector proxy support
- Key Transparency: added account data reset, additional logging around monitor versions
- Note: No changes to libsignal-protocol crate — does not affect this library's public API
5.0.0 - 2026-05-12 #
For Users #
✨ Highlights
- libsignal v0.94.0 — extends sender/recipient address binding to
SignalMessage.verifyMac() - libsignal_frb v4.0.0 — Rust FFI bindings updated with new sender/recipient address parameters on
verifyMac(breaking)
Changed
- Update libsignal native library to v0.94.0 (release notes)
- Breaking:
SignalMessage.verifyMac()now requiressenderAddressName,senderAddressDeviceId,recipientAddressName, andrecipientAddressDeviceIdparameters - Upstream made the previous
SignalMessage::verify_macmethod private and exposedverify_mac_with_addressesas the public replacement, extending the misdirection protection (started in v0.91.0) to message MAC verification
- Breaking:
4.0.1 - 2026-05-06 #
For Users #
✨ Highlights
- libsignal v0.93.2 — internal improvements and updates
- libsignal_frb v3.0.1 — Rust FFI bindings
Changed
- Update libsignal native library to v0.93.2 (compare)
- Networking improvements: H2 GOAWAY (graceful shutdown) handling for WebSockets
- Updated
hickory-protoDNS dependency to 0.26.1 - Updated CDSI production enclave and added new SVR enclaves (server-side)
- Note: No changes to libsignal-protocol crate — does not affect this library's public API
4.0.0 - 2026-05-01 #
For Users #
✨ Highlights
- libsignal v0.93.1 — extends sender/recipient address binding to remaining session APIs
- libsignal_frb v3.0.0 — Rust FFI bindings updated with new
localAddressparameter (breaking)
Changed
- Update libsignal native library to v0.93.1 (v0.93.0, v0.93.1)
- Breaking:
SessionBuilderconstructor now requireslocalAddressparameter - Breaking:
processPrekeyBundleWithCallbacksnow requireslocalNameandlocalDeviceIdparameters - Breaking:
messageDecryptSignalWithCallbacksnow requireslocalNameandlocalDeviceIdparameters process_prekey_bundleandmessage_decrypt_signalnow bind sender/recipient addresses, completing the misdirection protection introduced in v0.91.0
- Breaking:
3.0.3 - 2026-04-20 #
For Users #
✨ Highlights
- libsignal v0.92.2 — internal refactors and dependency updates
- libsignal_frb v2.0.2 — Rust FFI bindings (libsignal upstream bump)
Changed
- Update libsignal native library to v0.92.2 (compare)
- Internal refactor of 1:1 messaging code
- Key Transparency (keytrans) improvements: persist latest distinguished tree head, validate search responses
- Upgraded
randcrate andrustls-webpki - Note: These changes do not affect this library's public API
3.0.2 - 2026-04-12 #
For Users #
✨ Highlights
- libsignal v0.92.1 — SPQR v1 enforcement and dependency updates
- libsignal_frb v2.0.1 — updated native dependencies
Changed
3.0.1 - 2026-04-03 #
Fixed
- Fix README examples for
SessionCipherandSealedSenderCipherto match new API (addedlocalAddressand all required stores) - Fix incorrect class name
SealedSessionCipher→SealedSenderCipherin README - Fix incorrect method name
decryptPreKeySignalMessage→decryptPreKeyMessagein README
3.0.0 - 2026-04-03 #
For Users #
✨ Highlights
- libsignal v0.91.0 — message encryption now includes sender/recipient addresses in MAC for misdirection protection
- libsignal_frb v2.0.0 — Rust FFI bindings updated with new
localAddressparameter (breaking)
Changed
- Update libsignal native library to v0.91.0 (release notes)
- Breaking:
SessionCipherandSealedSenderCipherconstructors now requirelocalAddressparameter - Breaking:
messageEncryptWithCallbacksandmessageDecryptPrekeyWithCallbacksnow requirelocalNameandlocalDeviceIdparameters - Breaking:
sealedSenderDecryptWithCallbacksnow requireslocalNameandlocalDeviceIdparameters - 1:1 message encryption and decryption now includes sender/recipient addresses in the message MAC to prevent message misdirection attacks
- Backward compatible with messages from older clients that don't include addresses
- Breaking:
2.9.0 - 2026-03-29 #
For Users #
✨ Highlights
- libsignal v0.90.0 —
CiphertextMessagenow implementsClone - libsignal_frb v1.5.0 — Rust FFI bindings
Changed
- Update libsignal native library to v0.90.0 (release notes)
CiphertextMessageenum now derivesClone(previously onlyDebug)- Networking improvements: authenticated WebSocket message sending, key transparency API simplification
- Note: These changes do not affect this library's public API
- Update Flutter Rust Bridge to v2.12.0 (fix)
- Fixes web build compatibility with wasm-bindgen >=0.2.109
- Removed version pins for wasm-bindgen, js-sys, and web-sys
2.8.2 - 2026-03-25 #
For Users #
✨ Highlights
- libsignal v0.89.2 — dependency updates and networking improvements
- libsignal_frb v1.4.5 — Rust FFI bindings
Changed
- Update libsignal native library to v0.89.2 (release notes)
- Updated libcrux and SPQR (post-quantum) dependencies
- Updated rustls-webpki and tokio-util dependencies
- Networking improvements: service-level backoff, request cancellation
- Note: No changes to
libsignal-protocolcrate API — this library's public API is unaffected
2.8.1 - 2026-03-20 #
For Users #
✨ Highlights
- libsignal v0.89.1 — patch release with dependency updates
- libsignal_frb v1.4.4 — Rust FFI bindings
Changed
- Update libsignal native library to v0.89.1 (release notes)
- Patch release with internal dependency updates
- No public API changes
2.8.0 - 2026-03-18 #
For Users #
✨ Highlights
- libsignal v0.89.0 — internal improvements and updates
- libsignal_frb v1.4.3 — Rust FFI bindings
Changed
- Update libsignal native library to v0.89.0 (release notes)
- Internal improvements to the FFI bridge and callback mechanisms
- Enhanced backup/export functionalities
- Updates to keytrans handling
- Note: These changes do not affect this library's public API
2.7.2 - 2026-03-15 #
For Users #
✨ Highlights
- libsignal v0.88.3 — internal improvements and updates
- libsignal_frb v1.4.2 — Rust FFI bindings
Changed
- Update libsignal native library to v0.88.3 (release notes)
- Internal changes: FFI bridge callback improvements, backup/export refactoring, keytrans updates
- Note: These changes do not affect this library's public API
2.7.1 - 2026-03-07 #
For Users #
✨ Highlights
- libsignal v0.88.1 — internal bridge refactoring
- libsignal_frb v1.4.1 — Rust FFI bindings
Changed
- Update libsignal native library to v0.88.1 (release notes)
- Internal refactoring: further improvements to SenderKeyStore bridge implementations
- Note: These changes do not affect this library's public API
2.7.0 - 2026-03-03 #
For Users #
✨ Highlights
- libsignal v0.88.0 — internal bridge refactoring, no protocol changes
- libsignal_frb v1.4.0 — Rust FFI bindings
Changed
- Update libsignal native library to v0.88.0 (release notes)
- Internal refactoring: consolidated SenderKeyStore bridge implementations
- No changes to
libsignal-protocolcrate API — this library's public API is unaffected
2.6.0 - 2026-02-27 #
For Users #
✨ Highlights
- libsignal v0.87.5 — updated post-quantum cryptography dependencies
- libsignal_frb v1.3.0 — Rust FFI bindings
Changed
- Update libsignal native library to v0.87.5 (release notes)
- Updated SPQR (SparsePostQuantumRatchet) to v1.5.0
- Updated hpke-rs to v0.6.0 and libcrux-ml-kem to v0.0.7
- Added
zeroizesupport for HPKE Rng in signal-crypto - Note: These changes do not affect this library's public API
2.5.0 - 2026-02-21 #
For Users #
✨ Highlights
- libsignal v0.87.4 — updated BoringSSL and internal improvements
- libsignal_frb v1.2.0 — Rust FFI bindings
Changed
- Update libsignal native library to v0.87.4 (release notes)
- Updated
boringdependency to v5.0.1 (bundled BoringSSL update) - Added RemoteConfig for accountExists gRPC
- keytrans: removed search-with-version fallback from
monitor_and_search - Note: These changes do not affect this library's public API
- Updated
2.4.0 - 2026-02-18 #
For Users #
✨ Highlights
- libsignal v0.87.2 — security hardening for Diffie-Hellman key agreements
- libsignal_frb v1.1.0 — Rust FFI bindings
Security
- Update libsignal native library to v0.87.2 (release notes)
- Added validation of X25519 Diffie-Hellman shared secrets — rejects all-zero outputs per RFC 7748 §6.1, preventing potential use of predictable shared secrets from malicious low-order public keys
- Enabled overflow checks for release builds
- Updated BoringSSL to signalapp/boring v4.21.1
- Note: No changes to this library's public API
For Contributors #
Changed
- Adopt copier template v2.3.2 → v2.4.0
- Added Rust dependency caching (
Swatinem/rust-cache@v2) in CI setup-rust action — dramatically speeds up Windows builds (~10 min OpenSSL compile cached) - Added Strawberry Perl configuration for Windows CI to fix OpenSSL build (MSYS2 Perl from Git Bash is incompatible)
- Added
IPHONEOS_DEPLOYMENT_TARGETenv var for iOS CI builds — fixes linker errors when vendored C code is compiled with newer Xcode - Added
make check-targetscommand andscripts/check_deployment_targets.dartfor checking deployment target consistency (iOS/macOS/Android) across all project files - Added "Setting up Coverage Badge" and "Setting up pub.dev Publishing" sections to CONTRIBUTING.md
- Replaced
dart run scripts/withdart scripts/in Makefile commands, removing.skip_libsignal_hookworkaround (scripts only usedart:imports, sodart runbuild hooks are unnecessary) - Fixed WASM build hook: local builds now take priority over cached/downloaded files, avoiding stale content hash mismatches
- Added Rust dependency caching (
2.3.1 - 2026-02-11 #
For Users #
Changed
- Remove
flutterSDK constraint fromenvironment— pub.dev now displays both Dart and Flutter SDK badges (#14, thanks @ahnaineh)
For Contributors #
Changed
- Adopt copier template v2.2.0 → v2.3.2
- Publishing checklist now uses annotated tags (
git tag -a) instead of lightweight tags - Added
git push origin mainstep before pushing tag in publishing checklist - Replaced "Claude Commands" section with "Claude Skills" section in CLAUDE.md
- Removed redundant
prepare-releaseandupdate-templateClaude commands (functionality covered by Claude skills) - Updated platform support table in README: SDK 24+, iOS 13.0+, macOS 10.15+, WASM label
- Improved
frb-patternsClaude skill with additional patterns:- Added anti-pattern example to Constructor-Style API Pattern section
- Added Transparent Struct Pattern section
- Added Bridging Sync Traits to Async Callbacks section with
block_onexample - Added Adapter Pattern documentation for bridging DartFn callbacks to upstream traits
- Added
block_onpanics troubleshooting entry - Added "When to regenerate" checklist to Regenerating Bindings section
- Added No Threading on WASM warning
- Publishing checklist now uses annotated tags (
Fixed
- Restore 100% test coverage by adding
coverage:ignoremarkers to untestable platform-specific code inplatform_io.dart- AOT mode library loading path (unreachable during
dart testwhich runs in JIT mode) openLibraryFromPath()function (only called with customlibraryPath, already ignored at call site)
- AOT mode library loading path (unreachable during
2.3.0 - 2026-02-07 #
For Users #
✨ Highlights
- libsignal v0.87.1 — latest upstream native library
- libsignal_frb v1.0.3 — Rust FFI bindings
Changed
- Update libsignal native library to v0.87.1 (release notes)
CallLinkRootKeynow allows variable sizing; call link epochs removed from backup- Test infrastructure improvements (reusable session fuzz test support)
- Note: These changes do not affect this library's API
- Update
libsignal_frb(Rust crate) to v1.0.3
Security
- Updated
bytesdependency to v1.11.1 to address RUSTSEC-2026-0009
For Contributors #
Changed
- Adopt copier template (
copier-dart-frb-wrapper) v2.0.1 for project structure- Standardized scripts naming:
check_new_upstream_version.dart,check_exists_frb_release.dart - Unified common utilities in
scripts/src/common.dart - Renamed workflow:
build-libsignal-frb.yml→build-libsignal.yml - Configurable
version_tag_prefixfor upstream version tag handling - Improved version normalization in
check_updates.dart— supports configurable tag prefix instead of hardcodedvstripping
- Standardized scripts naming:
- Renamed
make update→make rust-updateto avoid ambiguity - Refactored build hook (
hook/build.dart)- Added SHA256 checksum verification for WASM downloads (supply chain security)
- Smarter app root detection: verifies pubspec depends on this package before copying WASM files
- WASM file caching with shared output directory (avoids redundant downloads)
- Incremental file copy: only copies if source is newer than destination
- Added
_crateNameconstant to eliminate hardcodedlibsignal_frbstrings - Added
rust/Cargo.tomlas dependency for cache invalidation on local builds - Improved error messages with actionable guidance throughout
- Replaced copier template placeholders with dynamic values from helper scripts
{{ android_min_sdk }}→ reads fromandroid/build.gradleat build time{{ crate_name }}→ uses_crateNameconstantfvm install→fvm usewith version from.fvmrc
- Updated example app platform configs to use template-standard naming
- Renamed
libsignal_example→examplein web, Windows, macOS, Linux, iOS configs
- Renamed
- Renamed Claude skill
ffi-patterns→frb-patternsto match current FRB architecture - Improved CI workflows with better step status tracking
- Each step now reports
success=true/falsefor clearer PR status - PR body shows inline status for each updated file
- Each step now reports
- Removed unused
GITHUB_TOKENfromcheck_updates.dart(not needed for public GitHub API) - Fully automated libsignal update workflow (
check-libsignal-updates.yml)- Now automatically runs
cargo updateto update Cargo.lock - Now automatically regenerates FRB bindings via
make codegen - Now automatically updates CHANGELOG.md using AI (requires
AI_MODELS_TOKENsecret withmodels:readpermission) - All steps are non-blocking: PR is created even if some steps fail
- PR description shows status of each step (success/failure)
- Labels added for failed steps (
cargo-toml-failed,cargo-lock-failed,codegen-failed,changelog-needed)
- Now automatically runs
Fixed
- Fix
workflow_runtrigger intest.yml— referenced wrong workflow name ("Build libsignal Native Libraries"→"Build libsignal FRB Libraries"), causing tests to never auto-trigger after build completion - Fix env var name in
build-libsignal.ymlcheck-release step (GH_TOKEN→GITHUB_TOKEN) — Dart script readsGITHUB_TOKEN, notGH_TOKEN - Fix outdated script filenames in
scripts/README.md(check_new_libsignal_version.dart→check_new_upstream_version.dart,check_exists_libsignal_frb_release.dart→check_exists_frb_release.dart) - Fix incorrect env var reference in
CLAUDE.mdinline comment (GITHUB_TOKEN→AI_MODELS_TOKEN) - Upgrade
flutter_lintsin example app from^5.0.0to^6.0.0 - Fix
.pubignore— include Rust source files in published package (only excluderust/target/build artifacts, not entirerust/directory); add trailing newline
Removed
- Removed legacy scripts with project-specific naming
scripts/check_new_libsignal_version.dart→scripts/check_new_upstream_version.dartscripts/check_exists_libsignal_frb_release.dart→scripts/check_exists_frb_release.dartscripts/src/check_new_libsignal_version.dart→scripts/src/check_updates.dart
- Removed unused
scripts/combine_artifacts.dart
Added
make check-template-updatescommand to check for new copier template versionscheck-template-updates.ymlworkflow — daily CI check for template updates with automated notification PRupdate-templateClaude skill — step-by-step guide for applying template updates- Documents
--defaultsflag for non-interactivecopier update(required for Claude Code) - Documents manual
_commitupdate in.copier-answers.ymlwhen copier fails to update it (conflicts or no file changes)
- Documents
make rust-updatecommand to updaterust/Cargo.lockviacargo updatemake update-changelogcommand to update CHANGELOG.md using GitHub Models AI- AI-powered changelog generation script (
scripts/update_changelog.dart)- Fetches libsignal release notes from GitHub API
- Uses GitHub Models (gpt-4o-mini) to generate appropriate changelog entry
- Includes real examples from project's CHANGELOG in AI prompt for consistent formatting
- Automatically inserts entry in correct CHANGELOG.md location
- Helper scripts for dynamic build configuration
scripts/get_android_min_sdk.dart— readsminSdkfromandroid/build.gradlescripts/get_flutter_version.dart— reads Flutter version from.fvmrc
- Analyzer exclusions for
hook/**,scripts/**,example/**,example_cli/**(separate packages, not part of main analysis)
2.2.1 - 2026-02-03 #
For Users #
Fixed
- Fix native library loading for pure Dart CLI applications
- JIT mode (
dart run): loads from.dart_tool/lib/ - AOT mode (
dart build cli): loads frombundle/lib/relative to executable - Enables standalone executables to be distributed and run from any location
- JIT mode (
Security
- Remove CWD-based library search to prevent library hijacking attacks
- Previously searched
rust/target/release/in current working directory - Attacker could place malicious library in CWD to hijack application
- Now only searches trusted paths: build hook locations and executable-relative paths
- Previously searched
2.2.0 - 2026-02-03 #
For Users #
✨ Highlights
- libsignal v0.87.0 — latest upstream Signal Protocol library
- libsignal_frb v1.0.2 — Rust FFI bindings
Changed
- Update libsignal native library to v0.87.0 (release notes)
- Breaking change in upstream:
PublicKeyordered comparison (Ord trait) has been removed - New:
accountExists()API exposed to client libraries - New: gRPC support for username hash lookup
- Note: Our
PublicKey.compare()method continues to work — now compares by serialized bytes
- Breaking change in upstream:
- Update
libsignal_frb(Rust crate) to v1.0.2- Adapted
PublicKey.compare()to use byte comparison after upstream Ord removal
- Adapted
Fixed
- Fix native library loading for pure Dart CLI applications using
dart runDynamicLibrary.open()doesn't resolve native asset IDs in JIT mode- Now reads
.dart_tool/native_assets.yamlto get the actual library path - Enables
example_cliand other CLI apps to work with published package
Security
- Updated
bytesdependency to v1.11.1 to fix integer overflow vulnerability (RUSTSEC-2026-0007)
For Contributors #
Added
make updatecommand to updaterust/Cargo.lockviacargo updatemake update-changelogcommand to update CHANGELOG.md using GitHub Models AI- AI-powered changelog generation script (
scripts/update_changelog.dart)- Fetches libsignal release notes from GitHub API
- Uses GitHub Models (gpt-4o-mini) to generate appropriate changelog entry
- Includes real examples from project's CHANGELOG in AI prompt for consistent formatting
- Automatically inserts entry in correct CHANGELOG.md location
Changed
- Fully automated libsignal update workflow (
check-libsignal-updates.yml)- Now automatically runs
cargo updateto update Cargo.lock - Now automatically regenerates FRB bindings via
make codegen - Now automatically updates CHANGELOG.md using AI (requires
AI_MODELS_TOKENsecret withmodels:readpermission) - All steps are non-blocking: PR is created even if some steps fail
- PR description shows status of each step (success/failure)
- Labels added for failed steps (
cargo-toml-failed,cargo-lock-failed,codegen-failed,changelog-needed) - Added checklist items for
rust/Cargo.tomlversion bump andmake rust-check
- Now automatically runs
- Updated
update_changelog.dartscript to generate two Highlights entries (libsignal + libsignal_frb) - Updated Claude skill
.claude/skills/update-libsignal/SKILL.mdwith "Review Automated PR" section
2.1.1 - 2026-01-30 #
For Users #
Changed
- Update libsignal native library to v0.86.16 (release notes)
- chat: Make gRPC failures directly convertible to RequestError
- Make E164Info and AciInfo constructors public
- Note: These changes do not affect this library's API
2.1.0 - 2026-01-29 #
For Users #
✨ Highlights
- libsignal v0.86.15 — latest upstream Signal Protocol library
Added
SecureBytesclass for wrapping sensitive byte data with automatic zeroing on disposalSecureUint8Listextension withzeroize()method for manual zeroing ofUint8List
Changed
- Update libsignal native library to v0.86.15 (release notes)
- SVR2: Updated production enclave
- SVRB: Added new production enclave to
currentset - New
accountExists()typed API - Backup: Support for key transparency fields
- Note: These changes are server-side infrastructure updates, no API changes affect this library
Security
- Rust-side zeroing of sensitive input bytes in all
deserialize()methods (keys, prekeys, sessions) - Added security documentation comments to methods returning sensitive data (serialize, agree, decrypt)
- Added zeroing best practices to SECURITY.md (Section J)
- Regenerated FRB bindings to include security documentation in Dart API
For Contributors #
Changed
- Remove unused
source_filesfrom iOS podspec- Native assets packages don't need CocoaPods to compile Swift code
- Libraries are loaded via
hook/build.dart, not CocoaPods - See Flutter docs
Fixed
- Fix Windows CI: download
makeandprotocfrom GitHub Releases instead of Chocolatey (CDN unreliable)
2.0.0 - 2026-01-24 #
For Users #
⚠️ Breaking Changes
-
Platform requirements: Minimum iOS raised to 13.0, macOS to 10.15
-
Architecture: Migrated from C FFI to Flutter Rust Bridge (FRB)
- No more
dispose()calls needed — memory managed automatically by Rust - Store operations now use DartFn callbacks for async Dart-to-Rust communication
- No more
-
API Changes:
ProtocolAddress('name', 1)→ProtocolAddress(name: 'name', deviceId: 1)privateKey.serialize().bytes→privateKey.serialize()(returnsUint8Listdirectly)publicKey.verify(message, signature)→publicKey.verify(message: message, signature: signature)Fingerprint.create(...)→Fingerprint(iterations: ..., version: ..., ...)Aes256GcmSiv(key)→Aes256GcmSiv(key: key)cipher.encrypt/decryptnow requiresassociatedDataparameterGroupSessionclass replaced with callback-based functions
✨ Highlights
- Web platform support (WASM) — run Signal Protocol in browsers
- Flutter Rust Bridge architecture — cleaner API, automatic memory management
- libsignal v0.86.14 — latest upstream Signal Protocol library
- Modern platform support — iOS 13.0+, macOS 10.15+ (Catalina)
Security
- Add low-order point validation for public keys in
PreKeyBundleandFingerprint- Reject non-canonical Curve25519 points that could be used in small subgroup attacks
Added
- Web platform support (WASM) — first-class browser support via wasm-pack
- Native assets build hooks (
hook/build.dart) for automatic library download - Precompiled binaries via GitHub Releases — no Rust required for end users
- SHA256 checksum verification for precompiled binaries
Changed
- Update libsignal native library to v0.86.14 (release notes)
- MSRV bumped to Rust 1.88
- Improve error message for unexpected ciphertext message types (now shows actual type)
Removed
SecureBytes,SerializationValidator,LibSignalExceptionclasses- Manual Dart wrapper classes (replaced by FRB-generated code)
For Contributors #
Added
make rust-audit— Rust dependency vulnerability scanningmake setup-rust-tools— installs cargo-audit, flutter_rust_bridge_codegenmake setup-protoc— cross-platform protoc installationmake setup-web— installs wasm-pack for web buildsmake setup-android— installs cargo-ndk for Android builds- Rust security audit job in CI (runs
cargo-auditon every test run) - Plaintext handling documentation in SECURITY.md
- CI workflow for building precompiled binaries (
build-libsignal-frb.yml)
Changed
- Update
.claude/skills/documentation for FRB architecture - Restructure
make setupto install all required tools
Removed
- Old C FFI code (
lib/src/bindings/,rust/src/ffi/) - Pre-built native libraries (
bin/,macos/Libraries/,ios/Libraries/, etc.) headers/signal_ffi.h
1.1.2 - 2026-01-19 #
Changed #
- Update libsignal native library to v0.86.12 (release notes)
- H2 support for unauthenticated chat (new remote config option)
- Updated libcrux-ml-kem and spqr dependencies
1.1.1 - 2026-01-13 #
Added #
.claude/skills/folder now included in repository and published package
Changed #
- Update libsignal native library to v0.86.11 (release notes)
- Fixes TLS proxy connectivity issue with certain TLS certificates
- Update FFI bindings to match new libsignal API:
- KyberPreKeyStore callbacks now include
destroycallback - Callback function names updated to longer namespaced format
- Parameter types updated (
SignalConstPointer*toSignalMutPointer*where applicable)
- KyberPreKeyStore callbacks now include
1.1.0 - 2026-01-08 #
Added #
- Add
make setup-buildcommand to install native build dependencies (Rust, protoc) - Add
make setup-fvmcommand (renamed from previousmake setup) - Restructure
make setupto run full setup (FVM + build dependencies) - Add "Skip Build Hook Pattern" documentation to CLAUDE.md
- Add multi-platform testing: Linux x86_64, Linux ARM64, macOS ARM64, Windows x86_64
- Add reusable test workflow (
test-reusable.yml) to eliminate code duplication betweentest.ymlandpublish.yml
Changed #
- Replace
softprops/action-gh-releasewith officialghCLI in CI workflows - Update GitHub Actions to latest versions:
actions/create-github-app-tokenv1 → v2peter-evans/create-pull-requestv7 → v8ilammy/msvc-dev-cmdv1 → v1.13.0
- Tests now run in parallel on all 4 platforms
- Extract test logic into reusable workflow for better maintainability
- Update libsignal native library to v0.86.10 (release notes)
- Simplify
check-libsignal-updates.ymlworkflow:- Remove AI analysis (GitHub Models) - now only updates
native_versionin pubspec.yaml - Remove automatic FFI bindings regeneration (now manual step after merge)
- Add clear instructions in PR body for manual steps after build completes
- Remove AI analysis (GitHub Models) - now only updates
- Simplify
check_updates.dartscript:- Remove
--ai,--no-ai,--bump,--no-changelogoptions - No longer updates package version or CHANGELOG.md automatically
- Remove
- Remove
scripts/src/ai_analysis.dart(no longer needed) - Use GitHub App token instead of
GITHUB_TOKENin workflows:check-libsignal-updates.yml: PR creationbuild-libsignal.yml: release version checks
- Skip tests for bot PRs in
test.yml(native libraries not yet built for version updates) - Discard FVM config changes in CI to prevent unwanted
.fvmrcand.vscode/settings.jsonmodifications in PRs - Extract Rust setup into reusable
.github/actions/setup-rustaction
Fixed #
- Fix duplicate "v" prefix in native library release notes (
vv0.86.10→v0.86.10) - Remove redundant "Usage" section from native library release description
- Fix ARM64 group messaging crash caused by
SignalUuid16-byte struct-by-value FFI limitation (dart-lang/sdk#36730)- Pass
SignalUuidas twoInt64values matching ARM64 AAPCS64 register layout - Affects
signal_sender_key_distribution_message_createandsignal_group_encrypt_message
- Pass
- Fix Windows native library build in CI
- Create shell wrapper for
fvminsetup-fvmaction (Git Bash cannot execute.batfiles) - Use PowerShell for build step to ensure MSVC
link.exeis used instead of Git's/usr/bin/link
- Create shell wrapper for
- Fix
make regenCI failure whencbindgenis not pre-installed - Fix
make regenCI failure due to missingprotoc(required by libsignal's spqr dependency) - Add
protocto build prerequisites documentation (README.md, CLAUDE.md)
1.0.1 - 2026-01-02 #
Added #
- Added
make doccommand for local API documentation generation - Added "Implementation Status" section to README.md with overview of wrapped native functionality
- Added pre-commit git hook for format check and static analysis (configured via
make setup) - Added
workflow_dispatchtrigger to test workflow (allows manual test runs from GitHub Actions)
Changed #
- Improved test coverage to 98.4%
- Added
// coverage:ignorecomments to genuinely untestable code (FFI callbacks, finalizers, defensive null checks) - Removed unused
extractOwnedBufferfunction fromFfiHelpers - Refactored CI update workflow: moved AI analysis from bash to Dart script
- Simplified
check-libsignal-updates.ymlworkflow (~530 → ~220 lines) - Added
--ai,--no-ai,--ciflags tocheck_updates.dartscript - Script now writes directly to
GITHUB_OUTPUTin CI mode (no jq parsing needed) build-libsignal.ymlworkflow now skips build if release already exists (prevents unnecessary rebuilds when only package version changes)
Fixed #
- Fixed
publish.ymlworkflow: use Flutter SDK (via FVM) instead of Dart SDK for publishing Flutter packages - Added
workflow_dispatchwith dry-run option to publish workflow - Added duplicate version check (validates against pub.dev API before publishing)
- Added
publish-dry-runvalidation step before actual publishing - Aligned publish workflow structure with liboqs_dart for consistency
- Fixed version parsing in
build-libsignal.ymlworkflow (use Dart script instead of grep for reliable parsing) - Fixed unresolved dartdoc references in
LibSignalException,GroupSession, andInMemoryIdentityKeyStore - Fixed
.pubignoreto includeCONTRIBUTING.mdin published package - Fixed
.pubignoreto exclude generateddoc/directory - Fixed LICENSE file format for proper pub.dev recognition (added full AGPL-3.0 text with SPDX identifier)
1.0.0 - 2025-12-31 #
Added #
- Pre-built native libraries for all platforms (iOS, Android, macOS, Linux, Windows)
- Signal Protocol: Double Ratchet algorithm for forward secrecy and break-in recovery
- X3DH: Extended Triple Diffie-Hellman for asynchronous key agreement
- Key Management: Curve25519 key pairs (
PrivateKey,PublicKey,IdentityKeyPair) - Pre-keys:
PreKeyRecord,SignedPreKeyRecord,PreKeyBundlefor session establishment - Post-quantum: Kyber key pairs (
KyberKeyPair,KyberPreKeyRecord) for quantum resistance - Sessions:
SessionRecord,ProtocolAddressfor session management - Messages:
SignalMessage,PreKeySignalMessagefor encrypted communication - Sealed Sender: Anonymous message sending (
ServerCertificate,SenderCertificate) - Group Messaging: SenderKey distribution (
GroupSession,SenderKeyRecord,SenderKeyDistributionMessage) - Cryptographic utilities: AES-256-GCM-SIV (
Aes256GcmSiv), HKDF (Hkdf), identity fingerprints (Fingerprint) - Storage interfaces:
SessionStore,IdentityKeyStore,PreKeyStore,SignedPreKeyStore,KyberPreKeyStore,SenderKeyStore - In-memory store implementations for testing and prototyping
- Automatic native library download via build hooks
- SHA256 verification for native library integrity
LibSignal.init()for optional library pre-initialization- Comprehensive exception handling with
SignalException - GitHub Actions CI/CD pipeline for automated testing and publishing
- Automated upstream version tracking with AI-powered changelog generation
- Cross-platform build scripts for native library compilation
- Example Flutter application and CLI example demonstrating all features
Security #
- Based on libsignal v0.86.11 from Signal Foundation
- Secret keys are handled securely with proper memory management
- Cryptographic operations use constant-time implementations where applicable