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())