Skip to content

Releases: open-e2ee/signal-protocol-js

v1.0.0

Choose a tag to compare

@jackspirou jackspirou released this 27 Aug 08:02
18f3979

@open-e2ee/signal-protocol-sdk@1.0.0 changelog:notes
node ./scripts/check-changelog.mjs --section 1.0.0

  • Hosted Relay supports data-only push wake hints across Expo, APNs, FCM,
    and Web Push.
    The SDK registers one destination for the authenticated
    device and derives no account, device, scope, or generation identifiers from
    application input. A wake starts an authenticated durable-mailbox pull.
    The SDK acknowledges only envelopes that it processes successfully, and the
    same pull remains available when push delivery is unavailable.

  • Hosted Relay now supports provider migration, recovery progress, and
    managed device linking.
    Migration assertions use the existing purpose-aware
    identity callback with typed source and target roles. Recovery reports typed
    phases while Relay remains the account authority. Managed linking requires a
    provisioned account identity. It also requires distinct active and new
    device-auth keys. It sends no ratchet, sender-key, skipped-key, or outbox
    state.

  • Breaking: sends use a durable exact-ciphertext outbox and always return a
    logical-send identifier.
    SendResult.clientMessageId is required. The SDK
    generates it when SendOptions.clientMessageId is absent, exposes it on send
    errors through isOutgoingMessageError, and replays the persisted encrypted
    transmission after an unknown Relay result. Receivers retain processed Relay
    envelope identifiers for 30 days and acknowledge duplicates without running
    protocol decryption again. Automatic sender-key rotation also persists its
    pairwise distribution messages before group transport. Custom
    ISignalProtocolLocalStore implementations must now implement
    deleteMetadata for bounded outbox and deduplication retention.

  • Breaking: linked-device backup format 2 transfers account identity only.
    A linked device generates fresh prekeys and establishes independent sessions.
    The SDK rejects old or extended backup shapes that carry prekeys, ratchet
    sessions, sender-key state, message state, or outbox state.

  • Profile-key credential issuance is blinded. The authenticated issuer
    receives an ACI-bound blinded request, not the raw profile key. The client
    unblinds and verifies the response before it creates a group presentation.

  • Breaking: sealed-sender delivery now uses one multi-recipient wire format
    for direct and group sends.
    The direct path supports one recipient through
    the same format, and the SDK rejects the retired V1 wire format. Hosted Relay
    adapters no longer implement the V1 send method. preferred delivery falls
    back to identified delivery only after an anonymous authorization rejection;
    other anonymous-delivery failures do not change the privacy route.

v0.4.0

Choose a tag to compare

@jackspirou jackspirou released this 19 Aug 19:02
a443711
  • The README now provides a runnable encrypted round trip and a current demo.
    The quickstart exercises two SDK clients through the in-memory relay. The new
    recording shows the same two-device and relay flow as the live desktop and
    mobile demo.

  • Error-surface checks reject unresolved codes and cover media attachment
    codes.
    The analyser now reports forwarded code arguments in CI.
    It also checks every MediaAttachmentErrorCode value.
    This release removes RemoteObjectStoreNotConfigured because no media
    operation could produce it.

  • The retry-request wire surface no longer includes
    RetryReason.INVALID_MESSAGE.
    No local failure produced it. The incoming
    retry handler did not support it. Malformed ciphertext failures use the supported
    DECRYPTION_FAILED recovery path.

  • Generated API pages and shipped documentation examples now have source
    gates.
    CI regenerates docs/api with pinned tool versions. It rejects a
    diff. The snippet runner scans every packaged Markdown file and the internal
    lifecycle docs. It executes every complete example and matches its output.
    For an example that cannot run, it compiles every SDK name against the packed
    package. A renamed or deleted export therefore fails the build in every
    document that names it.

    An example that previews an unreleased API declares the import paths that do
    not exist yet. The build fails if one of them resolves. Only pseudocode is
    exempt, and the build names each exempt block on its own line.

  • The error guide now documents all 33 EncryptionErrorCode values. Its
    session examples also use the current ProtocolAddress method signatures.

v0.3.0

Choose a tag to compare

@jackspirou jackspirou released this 18 Aug 13:58
a792f4d
  • Breaking: seven error classes and sixteen EncryptionErrorCode members are
    removed, because nothing in the library ever threw them.
    Each class was
    exported and paired with a type guard, so an application could branch on it —
    but no code path constructed any of them, which made every one of those
    branches dead code that read as handled. Removed, with their is* guards:
    IdentityKeyChangedError, SessionConflictError,
    RegistrationIdChangedError, and the four intermediate classes
    CryptoError, SessionError, ProtocolError and ClientError. Removed
    codes: IDENTITY_KEY_CHANGED, IDENTITY_KEY_ERROR, SESSION_CONFLICT,
    REGISTRATION_ID_CHANGED, INVALID_REGISTRATION_ID, MESSAGE_TOO_OLD,
    INVALID_MESSAGE_VERSION, RATCHET_ERROR, DATABASE_ERROR,
    DATABASE_LOCKED, KYBER_ERROR, KDF_ERROR, HMAC_VERIFICATION_FAILED,
    SPQR_KEY_ALREADY_USED, SPQR_EPOCH_REGRESSION and UNKNOWN_ERROR. The
    enum goes from 49 members to 33.

    What to use instead: an identity that changed is an identity that is not
    trusted, and the library throws UntrustedIdentityError /
    UNTRUSTED_IDENTITY for it from five sites on the session and sending
    paths. Code branching on isIdentityKeyChangedError or on
    IDENTITY_KEY_CHANGED was never reached and should branch on
    isUntrustedIdentityError instead. The other removed names have no
    replacement, because there was no event behind them.

  • UntrustedIdentityError and isUntrustedIdentityError are exported from
    the package root.
    They were reachable only from
    @open-e2ee/signal-protocol-sdk/types/errors, while the error-handling guide
    imported them from the root — so the first thing a reader copied did not
    resolve. StorageQuotaExceededError also gains the
    isStorageQuotaExceededError guard its five siblings already had.

  • Every documented error code is now traceable to a construction site. A
    check parses the library for the sites that decide whether a class can be
    thrown, and fails the build when an exported error class or a declared code
    survives with none — so the surface cannot drift back. Being named in a
    switch counted as use before, which is what hid IDENTITY_KEY_CHANGED
    through all nineteen releases that declared it and never threw it.

v0.2.3

Choose a tag to compare

@jackspirou jackspirou released this 13 Aug 04:47
8647fac
  • @noble/post-quantum 0.6.1 → 0.7.0. Upstream hardening of the
    primitives under the post-quantum ratchet: an intermediate-overflow fix
    in ML-KEM's NTT multiplication, stricter input validation throughout,
    and ML-KEM public-key cache detachment. The 0.7.0 breaking changes are
    confined to hybrid-KEM aliases and a utils export, neither of which this
    package imports. The bump also unifies the dependency tree on single
    copies of @noble/hashes and @noble/curves 2.3.0.

0.2.2

Choose a tag to compare

@jackspirou jackspirou released this 12 Aug 22:37
a0b80a1
  • The unfixable image-size development dependency is replaced with an
    inert local stub.
    Both open advisories against image-size cover every
    released version and have no fix. The package reaches the tree only
    through Metro's image-asset pipeline, which nothing in this repository
    exercises. An npm override now points it at a local package that throws
    on use, so the development install carries no known-vulnerable code and
    npm audit reports clean across the whole tree.

  • Continuous fuzzing runs against the parser seams. ClusterFuzzLite
    builds Jazzer.js fuzz targets over the wire-message, sender-key,
    sealed-sender, and post-quantum ratchet decoders, the base64 conversions,
    and the persistent session-record codec, on every push, pull request, and
    manual dispatch. Writing the targets' rejection contract surfaced one
    defect, fixed here: a session record with a malformed map encoding
    escaped the codec as a TypeError instead of its controlled error.

  • The README carries the OpenSSF Best Practices badge. The project
    earned the passing level at bestpractices.dev (project 14043).

  • SECURITY.md states the supported release line correctly. The
    supported-versions table had not moved past 0.1.x; it now names 0.2.x
    and states the no-backports rule.

0.2.1

Choose a tag to compare

@jackspirou jackspirou released this 12 Aug 20:04
07895bc
  • Two GitHub code-scanning findings in shipped code are fixed. The
    URL-safe base64 conversions stripped trailing = padding with a /=+$/
    replace, which backtracks quadratically on input that is mostly =
    characters; padding is now stripped by index in linear time. The Node
    store's getKey probed the key file with access() before reading it,
    leaving a race window between the check and the read; it now reads
    directly and treats ENOENT as an absent key. Neither fix changes
    observable behavior on well-formed input. The scan's prototype-pollution
    alerts against the Node sender-key trees were confirmed false positives:
    every decoded dictionary level is rebuilt with a null prototype before it
    is indexed, and the full-interface tests already prove __proto__ keys
    round-trip as plain data.

  • Vulnerable development-dependency versions are updated. js-yaml,
    nanoid, postcss, and brace-expansion move past their published
    denial-of-service advisories. The two image-size advisories have no
    fixed release yet; they sit behind Metro's dev-time asset parsing and do
    not reach the published package, whose production dependencies audit
    clean.

  • @noble/ciphers moves to 2.3.0. A routine minor update of the
    symmetric-cipher dependency, applied internally so the public Dependabot
    pull request can close against this release.

0.2.0

Choose a tag to compare

@jackspirou jackspirou released this 12 Aug 07:07
55bd85f
  • ML-KEM Braid key-agreement progress is now observable. A new
    protocolStrategy.onBraidProgress callback reports a BraidProgressEvent
    after every braid-mode send and receive: the chunks this side has carried in
    the current epoch, the chunks the open transfers account for, the epoch, and
    whether the operation produced the epoch secret. Braid mode spreads one
    ML-KEM key agreement across many messages, and until now the chunk counts
    lived entirely inside the state machine, so a host had no way to show or log
    how far a ratchet had travelled. A direct-mode session never raises the
    callback. The hook is guarded exactly as onProtocolSelected is: a consumer
    that throws is logged and the protocol path continues.

  • The web store's atomic commits leave no window for a partial commit
    under tab death.
    A dying page closes its IndexedDB connection
    gracefully, and a transaction that is idle at an await boundary then
    commits the writes it already holds. The atomic session/trust commit
    awaited each write in turn, so a tab death after the session write could
    commit the session while the consumed one-time prekey survived — CI
    observed exactly that once in Firefox. The identity-rotation commit
    deleted sessions through an awaited cursor walk, with the same windows
    between the new pin and each deletion. Both commits now enter every
    mutation in one synchronous batch before any settles, and a structural
    browser test fails on every run — no kill timing required — if a commit
    regains an await boundary between writes.

  • ML-KEM Braid key agreement completes. A braid session now reaches its
    first epoch on both sides; before this it could not complete one at all,
    failing at the 66th alternating message with Future epoch requested.
    Braid is the default SCKA mode, so this was the default path. The two sides
    derive an epoch secret at very different times by design — the encapsulator
    from a single Encaps1 over the header, the decapsulator only after
    Decaps over the full ct1 ‖ ct2 — and everything sent across that gap has
    to go out under the epoch the peer can still follow. spqrSend took that
    epoch from the chunk it was emitting, so it lost it on exactly the messages
    that carry no chunk: once its ciphertext is acknowledged, a sender has
    nothing left to send until the encapsulation-key transfer finishes. It then
    fell back to its own SPQR epoch, which had already advanced, and the peer
    rejected the message. The braid state machine reports the epoch it is
    sending under on every send, including the empty ones, and spqrSend now
    uses that for both the message-key derivation and the wire epoch. No wire
    format, serialized state, or public API changes.

  • Session state survives cloning under a cross-realm structuredClone.
    cloneProtocolState now checks once whether the ambient structuredClone
    returns objects of the calling realm, and falls back to its portable clone
    when it does not. Reached across a realm boundary — the arrangement a Node
    vm context produces, and one Jest builds for everything it runs —
    structuredClone returns a working Map whose prototype belongs to the
    other realm, so instanceof Map reports false for it everywhere downstream.
    Two silent losses followed: the session codec wrote such a Map as {},
    and the portable clone rebuilt it as a prototype-only husk whose size
    getter throws. ML-KEM Braid felt this first, because its decoders are the
    only protocol state held in a Map that has to survive many clones — a
    receiving decoder discarded every chunk it had accepted, held at one chunk
    forever, and the key agreement could never complete. Single-realm hosts, the
    browser and React Native included, always took and still take the native
    path.

  • Restoring a braid encoder state now rejects a malformed polynomial. Each
    polys element is a run of big-endian 16-bit coefficients. An odd-length
    element read one byte past the end. The resulting
    undefined folded into the low half of a coefficient. A truncated state
    therefore decoded to a polynomial with a fabricated final term. The decoder
    now refuses empty and odd-length elements, as the reference does.

  • Encoding a braid message now rejects a chunk that is not 32 bytes. The
    slot holds exactly 32 bytes. A shorter chunk left the rest of the slot zeroed and
    encoded a chunk the sender never held. A longer chunk raised a bounds error
    that named no field. Both now fail and report the length offered. The braid
    state machine only ever produces 32-byte chunks.

  • Deriving an SPQR send key now rejects a zeroed chain key. Two paths
    install one. An epoch advance for receiving alone installs a zeroed send
    chain key. Pruning writes one over each retired send chain. Deriving from
    either would key a message from a known constant. No production path reached
    that state, so the check is defence in depth.

  • The unused SPQR binary header codec is gone. serializeSPQRHeaderBinary
    and deserializeSPQRHeaderBinary emitted an unversioned framing. No
    production path used it. The documented wire format does not describe it. Use
    the protobuf codec, which carries the version capability.

0.1.0

Choose a tag to compare

@jackspirou jackspirou released this 10 Aug 16:32
7b36ae7
  • First non-prerelease release. Identical in code to 0.1.0-alpha.14;
    the version graduates the 0.1.0-alpha series. The alpha label is retired
    because the condition it described no longer holds: every shipped store
    adapter now carries the full ISignalProtocolLocalStore contract with its
    graduation checklist running as continuous CI gates — the storage contract
    suites in real Chromium, Firefox, and WebKit, the backend-conformance kit
    on Hermes, and the interruption, storage-pressure, multi-tab, and soak
    suites. The 0.x caveat stands: public APIs and persisted formats may
    change before 1.0.

v0.1.0-alpha.14

v0.1.0-alpha.14 Pre-release
Pre-release

Choose a tag to compare

@jackspirou jackspirou released this 10 Aug 03:22
b261323
  • ReactNativeSignalProtocolStore is no longer experimental. Every gate
    on its graduation checklist now runs continuously. A new
    backend-conformance kit — runBackendConformance and
    assertBackendConformance, exported from the package — executes thirteen
    cases against any ReactNativeKeyValueStorage implementation: round-trips,
    key listing, batch removal, in-order atomic application, checks evaluated
    against pre-batch state, null-guarded creation, all-or-nothing failure,
    exact-userId session removal with in-batch visibility, a single winner
    between concurrent guarded batches, and durability across reopen. Negative
    tests prove the kit catches a non-atomic backend, a prefix-matching
    session removal, and an unserialized backend. A new
    createReferenceReactNativeBackend — the executable specification of the
    backend contract — passes the kit on the Hermes engine React Native ships
    with, in a new CI gate that runs the bundled kit on the sha256-pinned
    Hermes CLI and requires an explicit pass sentinel, because Hermes exits 0
    on an unhandled async rejection. Interruption and storage-pressure jest
    suites drive the adapter over the reference backend: a simulated process
    kill before commit leaves every atomic security write all-or-nothing and
    a retry after reopen lands it whole, and quota exhaustion is exercised
    through every write path.

  • Quota exhaustion in the React Native store now surfaces as a typed
    error.
    The backend contract gains a documented quota signal: a backend
    rejects the failing write with an error named QuotaExceededError and
    commits nothing. The adapter maps that signal to the public
    StorageQuotaExceededError (code STORAGE_QUOTA_EXCEEDED) at the same
    adapter-boundary seam the browser store uses, so both stores report
    storage pressure identically.

0.1.0-alpha.13

0.1.0-alpha.13 Pre-release
Pre-release

Choose a tag to compare

@jackspirou jackspirou released this 10 Aug 00:57
f36b805
  • IndexedDbSignalProtocolStore is no longer experimental. Every gate
    on its graduation checklist now runs continuously: the storage contract
    suites in real Chromium, Firefox, and WebKit; the multi-tab
    compare-and-set suite; the interruption suite; the storage-pressure
    suite; and a new soak gate. The soak drives 2,000 full
    construct/initialize/write/read/close cycles through the adapter in one
    real Chromium page, samples renderer memory — including ArrayBuffer
    backing stores, which plain JS-heap readings miss — after forced
    garbage collection, and fails if the late-run median of memory or
    per-cycle latency grows beyond a small tolerance over the early-run
    median. Injecting a per-cycle leak into the adapter turns both
    detectors red. npm run soak:web-store runs longer sessions on demand.
    The React Native adapter remains experimental; its checklist is
    unchanged.

  • Quota exhaustion in the browser store now surfaces as a typed error.
    A write that exhausts the origin's storage quota rejects with the new
    public StorageQuotaExceededError (code STORAGE_QUOTA_EXCEEDED)
    instead of the raw engine QuotaExceededError. The mapping lives at the
    adapter boundary — one seam covering every store method, matched by
    error name so it holds across engines — and because every write commits
    in one atomic transaction, the typed error always means the write did
    not persist, never a partial record. A jest suite drives quota-shaped
    backend failures through every write path, and a new Chromium
    storage-pressure spec in the real-browser gate clamps the origin's
    quota, exhausts it against the real engine, and asserts the typed
    rejection, intact prior state, and a clean retry once space frees.

  • The real-browser gate now includes an interruption suite: a Playwright
    spec destroys a real tab while a write is in flight — an atomic
    session/trust commit, an identity rotation, a fresh-database bootstrap,
    and a prekey batch — then reopens the store from a fresh tab and asserts
    it is readable and each atomic security commit landed all-or-nothing,
    never partially. The kills are sampled at several points inside each
    write, in all three engines, and a deterministic structural test backs
    them: each atomic security commit must open exactly one readwrite
    transaction, which is what makes every kill land all-or-nothing. Tearing
    either commit into two transactions turns the structural test red on
    every run.

  • Two tabs no longer race the IndexedDB store into silent data loss. Two
    cross-connection races existed in the browser adapter. First, the database
    encryption key bootstrap read then wrote in separate transactions, so two
    tabs initializing a fresh database concurrently could each generate a
    different key — records written by the losing tab then failed authenticated
    decryption everywhere else. Second, first-contact trust pinning checked
    then wrote in separate transactions, so two tabs pinning different
    identities were both told NEW_IDENTITY while one pin silently replaced
    the other. Both paths now create through IndexedDB's add(), which refuses
    to overwrite: the bootstrap loser adopts the winner's key, and a
    first-contact loser re-evaluates its candidate against the pin that won.
    Rotation, verification, and the atomic session/trust commit already
    performed their compare-and-set inside a single transaction and are
    unchanged.

  • The real-browser gate now includes a multi-tab compare-and-set suite: a
    Playwright spec drives two tabs — two live IndexedDB connections on one
    origin — through concurrent key bootstrap, first-contact pinning,
    rotations, verification-versus-rotation, and one-time-prekey consumption,
    in all three engines. The two races above are what this suite caught, and
    reverting either fix turns it red.

  • Reading sessions or group sender keys no longer fails on Firefox and
    WebKit.
    getSessionsForUser and getAllSenderKeysForGroup in the
    IndexedDB store decrypted each row inside the cursor loop. An IndexedDB
    transaction deactivates the moment the event loop turns to anything that is
    not one of its own requests, so awaiting Web Crypto mid-iteration let the
    transaction commit and the next cursor advance threw
    TransactionInactiveError. Chromium's laxer transaction lifetime and the
    fake-indexeddb test double both tolerated the pattern, which is how it went
    unseen. Both methods now collect the encrypted rows inside the transaction
    and decrypt after it completes, the pattern the prekey reads already used.
    No key agreement, payload, or wire byte changed.

  • IndexedDbSignalProtocolStore.close(). The store held its database
    connection open for the life of the page, and an open connection blocks
    deletion and version upgrades of the database from anywhere else — so an
    application signing out or clearing local state had no supported way to
    release it. close() closes the connection and drops the in-memory
    database key; call initialize() again before any other operation.

  • The storage contract suites now also run against
    IndexedDbSignalProtocolStore inside real Chromium, Firefox, and WebKit
    pages on every change to the source repository. The suites are the same
    modules the jest gate runs; the Firefox and WebKit failures above are what
    the gate caught on its first run.