diff --git a/Cargo.lock b/Cargo.lock
index 25c60bd8c..aa6f8e23a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2608,14 +2608,14 @@ dependencies = [
[[package]]
name = "libsignal-debug"
-version = "0.93.3"
+version = "0.94.0"
dependencies = [
"cfg-if",
]
[[package]]
name = "libsignal-ffi"
-version = "0.93.3"
+version = "0.94.0"
dependencies = [
"cpufeatures 0.2.17",
"hex",
@@ -2636,7 +2636,7 @@ dependencies = [
[[package]]
name = "libsignal-jni"
-version = "0.93.3"
+version = "0.94.0"
dependencies = [
"libsignal-debug",
"libsignal-jni-impl",
@@ -2644,7 +2644,7 @@ dependencies = [
[[package]]
name = "libsignal-jni-impl"
-version = "0.93.3"
+version = "0.94.0"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
@@ -2661,7 +2661,7 @@ dependencies = [
[[package]]
name = "libsignal-jni-testing"
-version = "0.93.3"
+version = "0.94.0"
dependencies = [
"jni 0.21.1",
"libsignal-bridge-testing",
@@ -2985,7 +2985,7 @@ dependencies = [
[[package]]
name = "libsignal-node"
-version = "0.93.3"
+version = "0.94.0"
dependencies = [
"futures",
"libsignal-bridge",
diff --git a/Cargo.toml b/Cargo.toml
index aa4a72a2e..2223cc02d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,7 +38,7 @@ default-members = [
resolver = "2" # so that our dev-dependency features don't leak into products
[workspace.package]
-version = "0.93.3"
+version = "0.94.0"
authors = ["Signal Messenger LLC"]
license = "AGPL-3.0-only"
rust-version = "1.88"
diff --git a/LibSignalClient.podspec b/LibSignalClient.podspec
index cc3e889b3..081f65e64 100644
--- a/LibSignalClient.podspec
+++ b/LibSignalClient.podspec
@@ -5,7 +5,7 @@
Pod::Spec.new do |s|
s.name = 'LibSignalClient'
- s.version = '0.93.3'
+ s.version = '0.94.0'
s.summary = 'A Swift wrapper library for communicating with the Signal messaging service.'
s.homepage = 'https://github.com/signalapp/libsignal'
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 34e4d8156..8983dd222 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,4 +1,4 @@
-v0.93.3
+v0.94.0
- keytrans: Detect version changes sooner
-
+- Expose PQ session archiving ratio API
diff --git a/java/build.gradle b/java/build.gradle
index 15d28a4e7..c271049a9 100644
--- a/java/build.gradle
+++ b/java/build.gradle
@@ -23,7 +23,7 @@ repositories {
}
allprojects {
- version = "0.93.3"
+ version = "0.94.0"
group = "org.signal"
tasks.withType(JavaCompile) {
diff --git a/java/client/src/test/java/org/signal/libsignal/protocol/SessionBuilderTest.java b/java/client/src/test/java/org/signal/libsignal/protocol/SessionBuilderTest.java
index 5ec7dacab..616c2498f 100644
--- a/java/client/src/test/java/org/signal/libsignal/protocol/SessionBuilderTest.java
+++ b/java/client/src/test/java/org/signal/libsignal/protocol/SessionBuilderTest.java
@@ -300,8 +300,8 @@ public class SessionBuilderTest {
aliceSessionBuilder.process(bobPreKey, Instant.EPOCH);
SessionRecord initialSession = aliceStore.loadSession(BOB_ADDRESS);
- assertTrue(initialSession.hasSenderChain(Instant.EPOCH));
- assertFalse(initialSession.hasSenderChain(Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
+ assertTrue(initialSession.hasSenderChain(1.0, Instant.EPOCH));
+ assertFalse(initialSession.hasSenderChain(1.0, Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
String originalMessage = "Good, fast, cheap: pick two";
SessionCipher aliceSessionCipher = new SessionCipher(aliceStore, ALICE_ADDRESS, BOB_ADDRESS);
@@ -311,8 +311,8 @@ public class SessionBuilderTest {
assertTrue(outgoingMessage.getType() == CiphertextMessage.PREKEY_TYPE);
SessionRecord updatedSession = aliceStore.loadSession(BOB_ADDRESS);
- assertTrue(updatedSession.hasSenderChain(Instant.EPOCH));
- assertFalse(updatedSession.hasSenderChain(Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
+ assertTrue(updatedSession.hasSenderChain(1.0, Instant.EPOCH));
+ assertFalse(updatedSession.hasSenderChain(1.0, Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
try {
aliceSessionCipher.encrypt(
diff --git a/java/client/src/test/java/org/signal/libsignal/protocol/SessionRecordTest.java b/java/client/src/test/java/org/signal/libsignal/protocol/SessionRecordTest.java
index f14900093..92f50b657 100644
--- a/java/client/src/test/java/org/signal/libsignal/protocol/SessionRecordTest.java
+++ b/java/client/src/test/java/org/signal/libsignal/protocol/SessionRecordTest.java
@@ -8,8 +8,10 @@ package org.signal.libsignal.protocol;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
+import java.time.Instant;
import org.junit.Test;
import org.signal.libsignal.internal.NativeTesting;
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
@@ -29,7 +31,7 @@ public class SessionRecordTest {
public void testUninitAccess() {
SessionRecord empty_record = new SessionRecord();
- assertFalse(empty_record.hasSenderChain());
+ assertFalse(empty_record.hasSenderChain(1.0));
assertEquals(empty_record.getSessionVersion(), 0);
}
@@ -76,4 +78,25 @@ public class SessionRecordTest {
assertThrows(InvalidKeyException.class, () -> record.getKeyPair());
}
}
+
+ @Test
+ public void testHasUsablePQRatio() throws Exception {
+ // Record with key "\x7f\x7f\x7f\x7f....", so it's around a ratio of 0.5
+ SessionRecord recordNoPqRatchet =
+ new SessionRecord(
+ Hex.fromStringCondensedAssert(
+ "0a29080332006a207f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7a0101"));
+ assertTrue(recordNoPqRatchet.hasSenderChain(0.0, Instant.EPOCH));
+ assertTrue(recordNoPqRatchet.hasSenderChain(0.25, Instant.EPOCH));
+ assertFalse(recordNoPqRatchet.hasSenderChain(0.75, Instant.EPOCH));
+ assertFalse(recordNoPqRatchet.hasSenderChain(1.0, Instant.EPOCH));
+ SessionRecord recordWithPq =
+ new SessionRecord(
+ Hex.fromStringCondensedAssert(
+ "0a29080432006a207f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7a0101"));
+ assertTrue(recordWithPq.hasSenderChain(0.0, Instant.EPOCH));
+ assertTrue(recordWithPq.hasSenderChain(0.25, Instant.EPOCH));
+ assertTrue(recordWithPq.hasSenderChain(0.75, Instant.EPOCH));
+ assertTrue(recordWithPq.hasSenderChain(1.0, Instant.EPOCH));
+ }
}
diff --git a/java/shared/java/org/signal/libsignal/internal/Native.kt b/java/shared/java/org/signal/libsignal/internal/Native.kt
index e287bed7d..4a5fe3c85 100644
--- a/java/shared/java/org/signal/libsignal/internal/Native.kt
+++ b/java/shared/java/org/signal/libsignal/internal/Native.kt
@@ -1207,7 +1207,7 @@ internal object Native {
@JvmStatic @Throws(Exception::class)
public external fun SessionRecord_GetSessionVersion(s: ObjectHandle): Int
@JvmStatic @Throws(Exception::class)
- public external fun SessionRecord_HasUsableSenderChain(s: ObjectHandle, now: Long): Boolean
+ public external fun SessionRecord_HasUsableSenderChain(s: ObjectHandle, requirePqRatio: Double, now: Long): Boolean
@JvmStatic
public external fun SessionRecord_NewFresh(): ObjectHandle
@JvmStatic @Throws(Exception::class)
diff --git a/java/shared/java/org/signal/libsignal/protocol/state/SessionRecord.java b/java/shared/java/org/signal/libsignal/protocol/state/SessionRecord.java
index 9513d7a94..f6cdc2998 100644
--- a/java/shared/java/org/signal/libsignal/protocol/state/SessionRecord.java
+++ b/java/shared/java/org/signal/libsignal/protocol/state/SessionRecord.java
@@ -98,8 +98,8 @@ public class SessionRecord extends NativeHandleGuard.SimpleOwner {
*
*
If there is no current session, returns {@code false}.
*/
- public boolean hasSenderChain() {
- return hasSenderChain(Instant.now());
+ public boolean hasSenderChain(double requirePqRatio) {
+ return hasSenderChain(requirePqRatio, Instant.now());
}
/**
@@ -109,12 +109,13 @@ public class SessionRecord extends NativeHandleGuard.SimpleOwner {
*
*
You should only use this overload if you need to test session expiration.
*/
- public boolean hasSenderChain(Instant now) {
+ public boolean hasSenderChain(double requirePqRatio, Instant now) {
return filterExceptions(
() ->
guardedMapChecked(
(nativeHandle) ->
- Native.SessionRecord_HasUsableSenderChain(nativeHandle, now.toEpochMilli())));
+ Native.SessionRecord_HasUsableSenderChain(
+ nativeHandle, requirePqRatio, now.toEpochMilli())));
}
public boolean currentRatchetKeyMatches(ECPublicKey key) {
diff --git a/node/package-lock.json b/node/package-lock.json
index 9cb778ab4..f3f79316d 100644
--- a/node/package-lock.json
+++ b/node/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@signalapp/libsignal-client",
- "version": "0.93.3",
+ "version": "0.94.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@signalapp/libsignal-client",
- "version": "0.93.3",
+ "version": "0.94.0",
"hasInstallScript": true,
"license": "AGPL-3.0-only",
"dependencies": {
@@ -873,6 +873,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz",
"integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==",
"dev": true,
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.44.0",
"@typescript-eslint/types": "8.44.0",
@@ -1070,6 +1071,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1472,6 +1474,7 @@
"resolved": "https://registry.npmjs.org/chai/-/chai-6.0.1.tgz",
"integrity": "sha512-/JOoU2//6p5vCXh00FpNgtlw0LjvhGttaWc+y7wpW9yjBm3ys0dI8tSKZxIOgNruz5J0RleccatSIC3uxEZP0g==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -2067,6 +2070,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz",
"integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
"dev": true,
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -2226,6 +2230,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -4396,6 +4401,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
+ "peer": true,
"bin": {
"prettier": "bin-prettier.js"
},
@@ -5119,6 +5125,7 @@
"resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz",
"integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==",
"dev": true,
+ "peer": true,
"dependencies": {
"@sinonjs/commons": "^3.0.1",
"@sinonjs/fake-timers": "^13.0.5",
@@ -5542,6 +5549,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -5725,6 +5733,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/node/package.json b/node/package.json
index 69f0a0f56..e0064b57a 100644
--- a/node/package.json
+++ b/node/package.json
@@ -1,6 +1,6 @@
{
"name": "@signalapp/libsignal-client",
- "version": "0.93.3",
+ "version": "0.94.0",
"repository": "github:signalapp/libsignal",
"license": "AGPL-3.0-only",
"type": "module",
diff --git a/node/ts/Native.ts b/node/ts/Native.ts
index 74b3c82a5..4c164b446 100644
--- a/node/ts/Native.ts
+++ b/node/ts/Native.ts
@@ -294,7 +294,7 @@ type NativeFunctions = {
CiphertextMessage_Serialize: (obj: Wrapper) => Uint8Array;
CiphertextMessage_FromPlaintextContent: (m: Wrapper) => CiphertextMessage;
SessionRecord_ArchiveCurrentState: (sessionRecord: Wrapper) => void;
- SessionRecord_HasUsableSenderChain: (s: Wrapper, now: Timestamp) => boolean;
+ SessionRecord_HasUsableSenderChain: (s: Wrapper, requirePqRatio: number, now: Timestamp) => boolean;
SessionRecord_CurrentRatchetKeyMatches: (s: Wrapper, key: Wrapper) => boolean;
SessionRecord_Deserialize: (data: Uint8Array) => SessionRecord;
SessionRecord_Serialize: (obj: Wrapper) => Uint8Array;
diff --git a/node/ts/index.ts b/node/ts/index.ts
index d5a19f757..52840eb19 100644
--- a/node/ts/index.ts
+++ b/node/ts/index.ts
@@ -518,8 +518,12 @@ export class SessionRecord {
*
* If there is no current session, returns false.
*/
- hasCurrentState(now: Date = new Date()): boolean {
- return Native.SessionRecord_HasUsableSenderChain(this, now.getTime());
+ hasCurrentState(requirePqRatio: number, now: Date = new Date()): boolean {
+ return Native.SessionRecord_HasUsableSenderChain(
+ this,
+ requirePqRatio,
+ now.getTime()
+ );
}
currentRatchetKeyMatches(key: PublicKey): boolean {
diff --git a/node/ts/test/protocol/ProtocolTest.ts b/node/ts/test/protocol/ProtocolTest.ts
index 70abe6ee5..5f87be584 100644
--- a/node/ts/test/protocol/ProtocolTest.ts
+++ b/node/ts/test/protocol/ProtocolTest.ts
@@ -812,7 +812,7 @@ for (const testCase of sessionVersionTestCases) {
assert(session.serialize().length > 0);
assert.deepEqual(session.localRegistrationId(), 5);
assert.deepEqual(session.remoteRegistrationId(), 5);
- assert(session.hasCurrentState());
+ assert(session.hasCurrentState(1.0));
assert(
!session.currentRatchetKeyMatches(
SignalClient.PrivateKey.generate().getPublicKey()
@@ -820,7 +820,7 @@ for (const testCase of sessionVersionTestCases) {
);
session.archiveCurrentState();
- assert(!session.hasCurrentState());
+ assert(!session.hasCurrentState(1.0));
assert(
!session.currentRatchetKeyMatches(
SignalClient.PrivateKey.generate().getPublicKey()
@@ -968,8 +968,12 @@ for (const testCase of sessionVersionTestCases) {
);
const initialSession = await aliceStores.session.getSession(bAddress);
- assert.isTrue(initialSession?.hasCurrentState(new Date('2020-01-01')));
- assert.isFalse(initialSession?.hasCurrentState(new Date('2023-01-01')));
+ assert.isTrue(
+ initialSession?.hasCurrentState(1.0, new Date('2020-01-01'))
+ );
+ assert.isFalse(
+ initialSession?.hasCurrentState(1.0, new Date('2023-01-01'))
+ );
const aMessage = Buffer.from('Greetings hoo-man', 'utf8');
const aCiphertext = await SignalClient.signalEncrypt(
@@ -987,8 +991,12 @@ for (const testCase of sessionVersionTestCases) {
);
const updatedSession = await aliceStores.session.getSession(bAddress);
- assert.isTrue(updatedSession?.hasCurrentState(new Date('2020-01-01')));
- assert.isFalse(updatedSession?.hasCurrentState(new Date('2023-01-01')));
+ assert.isTrue(
+ updatedSession?.hasCurrentState(1.0, new Date('2020-01-01'))
+ );
+ assert.isFalse(
+ updatedSession?.hasCurrentState(1.0, new Date('2023-01-01'))
+ );
await assert.isRejected(
SignalClient.signalEncrypt(
diff --git a/rust/bridge/jni/bin/gen_java_decl.py b/rust/bridge/jni/bin/gen_java_decl.py
index c74ba227e..79a94f2f5 100755
--- a/rust/bridge/jni/bin/gen_java_decl.py
+++ b/rust/bridge/jni/bin/gen_java_decl.py
@@ -82,6 +82,7 @@ def translate_to_java(typ: str) -> Tuple[str, bool]:
'Nullable': 'ObjectHandle',
'jint': 'Int',
'jlong': 'Long',
+ 'jdouble': 'Double',
'jboolean': 'Boolean',
'JObject': 'Object',
'JClass': 'Class<*>',
diff --git a/rust/bridge/node/bin/gen_ts_decl.py b/rust/bridge/node/bin/gen_ts_decl.py
index 3bf867b6d..195a9a996 100755
--- a/rust/bridge/node/bin/gen_ts_decl.py
+++ b/rust/bridge/node/bin/gen_ts_decl.py
@@ -70,6 +70,7 @@ def translate_to_ts(typ: str) -> str:
'u16': 'number',
'u32': 'number',
'u64': 'bigint',
+ 'f64': 'number',
'bool': 'boolean',
'String': 'string',
'&str': 'string',
diff --git a/rust/bridge/shared/src/protocol.rs b/rust/bridge/shared/src/protocol.rs
index d5d21a6d4..2ad0972d0 100644
--- a/rust/bridge/shared/src/protocol.rs
+++ b/rust/bridge/shared/src/protocol.rs
@@ -974,8 +974,35 @@ fn SessionRecord_ArchiveCurrentState(session_record: &mut SessionRecord) -> Resu
}
#[bridge_fn]
-fn SessionRecord_HasUsableSenderChain(s: &SessionRecord, now: Timestamp) -> Result {
- s.has_usable_sender_chain(now.into(), SessionUsabilityRequirements::NotStale)
+fn SessionRecord_HasUsableSenderChain(
+ s: &SessionRecord,
+ require_pq_ratio: f64,
+ now: Timestamp,
+) -> Result {
+ let has_chain =
+ s.has_usable_sender_chain(now.into(), SessionUsabilityRequirements::NotStale)?;
+ if !has_chain {
+ return Ok(false);
+ }
+ let has_pq_chain = s.has_usable_sender_chain(
+ now.into(),
+ SessionUsabilityRequirements::NotStale
+ | SessionUsabilityRequirements::EstablishedWithPqxdh
+ | SessionUsabilityRequirements::Spqr,
+ )?;
+ if has_pq_chain || require_pq_ratio == 0.0 {
+ return Ok(true);
+ }
+ let require_pq_ratio = if require_pq_ratio > 1.0 {
+ log::warn!("pinning overly high PQ ratio {require_pq_ratio} to 1.0");
+ 1.0
+ } else if require_pq_ratio < 0.0 {
+ log::warn!("pinning overly low PQ ratio {require_pq_ratio} to 0.0");
+ 0.0
+ } else {
+ require_pq_ratio
+ };
+ Ok(should_use_nonpq_session(require_pq_ratio, s.alice_base_key().expect("we should have a current session, since has_usable_sender_chain returned a non-error value")))
}
#[bridge_fn]
diff --git a/rust/bridge/shared/types/src/ffi/convert.rs b/rust/bridge/shared/types/src/ffi/convert.rs
index 70ebe075d..fcc56a676 100644
--- a/rust/bridge/shared/types/src/ffi/convert.rs
+++ b/rust/bridge/shared/types/src/ffi/convert.rs
@@ -1429,6 +1429,7 @@ trivial!(u64);
trivial!(i64);
trivial!(usize);
trivial!(bool);
+trivial!(f64);
/// Syntactically translates `bridge_fn` argument types (and callback result types) to FFI types for
/// `cbindgen`.
@@ -1444,6 +1445,7 @@ macro_rules! ffi_arg_type {
(i32) => (i32);
(u32) => (u32);
(u64) => (u64);
+ (f64) => (f64);
(Option) => (u32);
(usize) => (usize);
(bool) => (bool);
@@ -1549,6 +1551,7 @@ macro_rules! ffi_result_type {
(Option) => (u32);
(u64) => (u64);
(i64) => (i64);
+ (f64) => (f64);
(Option) => (u64);
(bool) => (bool);
(&str) => (*const std::ffi::c_char);
diff --git a/rust/bridge/shared/types/src/jni/convert.rs b/rust/bridge/shared/types/src/jni/convert.rs
index 8c257c2d6..f1e8a3aa1 100644
--- a/rust/bridge/shared/types/src/jni/convert.rs
+++ b/rust/bridge/shared/types/src/jni/convert.rs
@@ -239,6 +239,13 @@ impl SimpleArgTypeInfo<'_> for u64 {
}
}
+impl SimpleArgTypeInfo<'_> for f64 {
+ type ArgType = jdouble;
+ fn convert_from(_env: &mut JNIEnv, foreign: &jdouble) -> Result {
+ Ok(*foreign)
+ }
+}
+
/// Supports values `0..=Long.MAX_VALUE`.
///
/// Negative `long` values are *not* reinterpreted as large `u64` values.
@@ -2677,6 +2684,9 @@ macro_rules! jni_arg_type {
(u64) => {
::jni::sys::jlong
};
+ (f64) => {
+ ::jni::sys::jdouble
+ };
(bool) => {
::jni::sys::jboolean
};
diff --git a/rust/bridge/shared/types/src/jni/mod.rs b/rust/bridge/shared/types/src/jni/mod.rs
index 9eae7d913..211e0cabe 100644
--- a/rust/bridge/shared/types/src/jni/mod.rs
+++ b/rust/bridge/shared/types/src/jni/mod.rs
@@ -20,7 +20,7 @@ pub use jni::objects::{
JValueOwned, ReleaseMode,
};
use jni::objects::{GlobalRef, JThrowable};
-pub use jni::sys::{jboolean, jint, jlong};
+pub use jni::sys::{jboolean, jdouble, jint, jlong};
use libsignal_account_keys::Error as PinError;
use libsignal_core::try_scoped;
use libsignal_net::chat::{ConnectError as ChatConnectError, SendError as ChatSendError};
diff --git a/rust/bridge/shared/types/src/node/convert.rs b/rust/bridge/shared/types/src/node/convert.rs
index 7117d03f9..67b6f2cd4 100644
--- a/rust/bridge/shared/types/src/node/convert.rs
+++ b/rust/bridge/shared/types/src/node/convert.rs
@@ -359,6 +359,13 @@ impl SimpleArgTypeInfo for u64 {
}
}
+impl SimpleArgTypeInfo for f64 {
+ type ArgType = JsNumber;
+ fn convert_from(cx: &mut FunctionContext, foreign: Handle) -> NeonResult {
+ Ok(foreign.value(cx))
+ }
+}
+
impl SimpleArgTypeInfo for String {
type ArgType = JsString;
fn convert_from(cx: &mut FunctionContext, foreign: Handle) -> NeonResult {
diff --git a/rust/core/src/version.rs b/rust/core/src/version.rs
index 3c14a6d43..c93f0f65c 100644
--- a/rust/core/src/version.rs
+++ b/rust/core/src/version.rs
@@ -5,4 +5,4 @@
// The value of this constant is updated by the script
// and should not be manually modified
-pub const VERSION: &str = "0.93.3";
+pub const VERSION: &str = "0.94.0";
diff --git a/rust/protocol/src/lib.rs b/rust/protocol/src/lib.rs
index c739fb6e8..3b5aeefc0 100644
--- a/rust/protocol/src/lib.rs
+++ b/rust/protocol/src/lib.rs
@@ -65,6 +65,7 @@ pub use protocol::{
CiphertextMessage, CiphertextMessageType, DecryptionErrorMessage, KyberPayload,
PlaintextContent, PreKeySignalMessage, SenderKeyDistributionMessage, SenderKeyMessage,
SignalMessage, extract_decryption_error_message_from_serialized_content,
+ should_use_nonpq_session,
};
pub use ratchet::{
AliceSignalProtocolParameters, BobSignalProtocolParameters, initialize_alice_session_record,
diff --git a/rust/protocol/src/protocol.rs b/rust/protocol/src/protocol.rs
index 4046c03c7..76958ccdf 100644
--- a/rust/protocol/src/protocol.rs
+++ b/rust/protocol/src/protocol.rs
@@ -981,6 +981,39 @@ pub fn extract_decryption_error_message_from_serialized_content(
.and_then(DecryptionErrorMessage::try_from)
}
+/// A consistent way to determine, given a session that is not PQ
+/// and a ratio of sessions which if not PQ should be archived,
+/// which sessions to use (returning true) and which to archive
+/// (returning false). The session key's first 4 bytes are used as
+/// a uniformly random big-endian integer as part of this calculation,
+/// which works well for a session's `alice_base_key()`.
+pub fn should_use_nonpq_session(require_pq_ratio: f64, session_key: &[u8]) -> bool {
+ assert!(session_key.len() >= 4);
+ if require_pq_ratio >= 1.0 {
+ return false;
+ } else if require_pq_ratio <= 0.0 {
+ return true;
+ }
+ // We have a chain, but it's not a PQ chain.
+ // We want to deterministically decide whether a session should be used
+ // based on a ratio between 0 and 1. We also want the decision as to
+ // whether to use the session to be the same for Alice and Bob.
+ // The session key is a x25519 key, from which we pull out 4 bytes
+ // we expect to be relatively uniform.
+ let sess_u32 = u32::from_be_bytes(
+ (&session_key[..4])
+ .try_into()
+ .expect("should have 32 bytes"),
+ );
+ // We then convert the require_pq_ratio to a u32 that is 0xFF... for 1,
+ // 0x00... for 0, and uniform in between for other values.
+ #[allow(clippy::cast_possible_truncation)]
+ let ratio_u32 = ((u32::MAX as f64) * require_pq_ratio) as u32;
+ // Finally, we compare the two, and we only expire the existing session if
+ // its key is smaller than the ratio key.
+ ratio_u32 <= sess_u32
+}
+
#[cfg(test)]
mod tests {
use rand::rngs::OsRng;
@@ -1360,4 +1393,23 @@ mod tests {
Err(SignalProtocolError::InvalidArgument(_))
));
}
+
+ #[test]
+ fn test_should_use_nonpq_session() {
+ let max = b"\xff\xff\xff\xff";
+ let min = b"\x00\x00\x00\x00";
+ let mid = b"\x7f\xff\xff\xff";
+ assert!(!should_use_nonpq_session(1.0, max));
+ assert!(!should_use_nonpq_session(1.0, min));
+ assert!(should_use_nonpq_session(0.0, max));
+ assert!(should_use_nonpq_session(0.0, min));
+
+ assert!(!should_use_nonpq_session(0.75, min));
+ assert!(!should_use_nonpq_session(0.75, mid));
+ assert!(should_use_nonpq_session(0.75, max));
+
+ assert!(!should_use_nonpq_session(0.25, min));
+ assert!(should_use_nonpq_session(0.25, mid));
+ assert!(should_use_nonpq_session(0.25, max));
+ }
}
diff --git a/swift/.swiftlint.yml b/swift/.swiftlint.yml
index eeb9458ea..e5f1192bb 100644
--- a/swift/.swiftlint.yml
+++ b/swift/.swiftlint.yml
@@ -2,6 +2,7 @@ disabled_rules:
- closure_parameter_position # swift-format takes precedence here
- cyclomatic_complexity
- empty_enum_arguments
+- file_length
- force_try
- function_body_length
- function_parameter_count
diff --git a/swift/Sources/LibSignalClient/state/SessionRecord.swift b/swift/Sources/LibSignalClient/state/SessionRecord.swift
index 3e3841df9..d7712844c 100644
--- a/swift/Sources/LibSignalClient/state/SessionRecord.swift
+++ b/swift/Sources/LibSignalClient/state/SessionRecord.swift
@@ -39,18 +39,15 @@ public class SessionRecord: ClonableHandleOwner {
}
}
- public var hasCurrentState: Bool {
- hasCurrentState(now: Date())
- }
-
- public func hasCurrentState(now: Date) -> Bool {
+ public func hasCurrentState(requirePqRatio: Double, now: Date = Date()) -> Bool {
return self.withNativeHandle { nativeHandle in
failOnError {
try invokeFnReturningBool {
signal_session_record_has_usable_sender_chain(
$0,
nativeHandle.const(),
- UInt64(now.timeIntervalSince1970 * 1000)
+ requirePqRatio,
+ UInt64(now.timeIntervalSince1970 * 1000),
)
}
}
diff --git a/swift/Sources/SignalFfi/signal_ffi.h b/swift/Sources/SignalFfi/signal_ffi.h
index a48364906..d94539f8a 100644
--- a/swift/Sources/SignalFfi/signal_ffi.h
+++ b/swift/Sources/SignalFfi/signal_ffi.h
@@ -2719,7 +2719,7 @@ SignalFfiError *signal_session_record_get_local_registration_id(uint32_t *out, S
SignalFfiError *signal_session_record_get_remote_registration_id(uint32_t *out, SignalConstPointerSessionRecord obj);
-SignalFfiError *signal_session_record_has_usable_sender_chain(bool *out, SignalConstPointerSessionRecord s, uint64_t now);
+SignalFfiError *signal_session_record_has_usable_sender_chain(bool *out, SignalConstPointerSessionRecord s, double require_pq_ratio, uint64_t now);
SignalFfiError *signal_session_record_serialize(SignalOwnedBuffer *out, SignalConstPointerSessionRecord obj);
diff --git a/swift/Tests/LibSignalClientTests/SessionTests.swift b/swift/Tests/LibSignalClientTests/SessionTests.swift
index feca6178c..34b7a0cc3 100644
--- a/swift/Tests/LibSignalClientTests/SessionTests.swift
+++ b/swift/Tests/LibSignalClientTests/SessionTests.swift
@@ -188,8 +188,15 @@ class SessionTests: TestCaseBase {
)
let initial_session = try! alice_store.loadSession(for: bob_address, context: NullContext())!
- XCTAssertTrue(initial_session.hasCurrentState(now: Date(timeIntervalSinceReferenceDate: 0)))
- XCTAssertFalse(initial_session.hasCurrentState(now: Date(timeIntervalSinceReferenceDate: 60 * 60 * 24 * 90)))
+ XCTAssertTrue(
+ initial_session.hasCurrentState(requirePqRatio: 1.0, now: Date(timeIntervalSinceReferenceDate: 0))
+ )
+ XCTAssertFalse(
+ initial_session.hasCurrentState(
+ requirePqRatio: 1.0,
+ now: Date(timeIntervalSinceReferenceDate: 60 * 60 * 24 * 90)
+ )
+ )
// Alice sends a message:
let ptext_a: [UInt8] = [8, 6, 7, 5, 3, 0, 9]
@@ -207,8 +214,15 @@ class SessionTests: TestCaseBase {
XCTAssertEqual(ctext_a.messageType, .preKey)
let updated_session = try! alice_store.loadSession(for: bob_address, context: NullContext())!
- XCTAssertTrue(updated_session.hasCurrentState(now: Date(timeIntervalSinceReferenceDate: 0)))
- XCTAssertFalse(updated_session.hasCurrentState(now: Date(timeIntervalSinceReferenceDate: 60 * 60 * 24 * 90)))
+ XCTAssertTrue(
+ updated_session.hasCurrentState(requirePqRatio: 1.0, now: Date(timeIntervalSinceReferenceDate: 0))
+ )
+ XCTAssertFalse(
+ updated_session.hasCurrentState(
+ requirePqRatio: 1.0,
+ now: Date(timeIntervalSinceReferenceDate: 60 * 60 * 24 * 90)
+ )
+ )
XCTAssertThrowsError(
try signalEncrypt(
@@ -502,14 +516,14 @@ class SessionTests: TestCaseBase {
let session: SessionRecord! = try! alice_store.loadSession(for: bob_address, context: NullContext())
XCTAssertNotNil(session)
- XCTAssertTrue(session.hasCurrentState)
+ XCTAssertTrue(session.hasCurrentState(requirePqRatio: 1.0))
XCTAssertFalse(try! session.currentRatchetKeyMatches(IdentityKeyPair.generate().publicKey))
session.archiveCurrentState()
- XCTAssertFalse(session.hasCurrentState)
+ XCTAssertFalse(session.hasCurrentState(requirePqRatio: 1.0))
XCTAssertFalse(try! session.currentRatchetKeyMatches(IdentityKeyPair.generate().publicKey))
// A redundant archive shouldn't break anything.
session.archiveCurrentState()
- XCTAssertFalse(session.hasCurrentState)
+ XCTAssertFalse(session.hasCurrentState(requirePqRatio: 1.0))
}
func testSealedSenderGroupCipher() throws {
@@ -953,7 +967,10 @@ private func initializeSessionsV4(
context: NullContext()
)
- XCTAssertEqual(try! alice_store.loadSession(for: bob_address, context: NullContext())?.hasCurrentState, true)
+ XCTAssertEqual(
+ try! alice_store.loadSession(for: bob_address, context: NullContext())?.hasCurrentState(requirePqRatio: 1.0),
+ true
+ )
XCTAssertEqual(
try! alice_store.loadSession(for: bob_address, context: NullContext())?.remoteRegistrationId(),
try! bob_store.localRegistrationId(context: NullContext())