SPQR: API changes to allow remote config for archiving non-PQ sessions.

This commit is contained in:
gram-signal 2026-05-08 14:06:24 -07:00 committed by GitHub
parent 9adf4191f0
commit 2486ffe4e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 214 additions and 52 deletions

12
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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'

View File

@ -1,4 +1,4 @@
v0.93.3
v0.94.0
- keytrans: Detect version changes sooner
- Expose PQ session archiving ratio API

View File

@ -23,7 +23,7 @@ repositories {
}
allprojects {
version = "0.93.3"
version = "0.94.0"
group = "org.signal"
tasks.withType(JavaCompile) {

View File

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

View File

@ -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));
}
}

View File

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

View File

@ -98,8 +98,8 @@ public class SessionRecord extends NativeHandleGuard.SimpleOwner {
*
* <p>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 {
*
* <p>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) {

13
node/package-lock.json generated
View File

@ -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"

View File

@ -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",

View File

@ -294,7 +294,7 @@ type NativeFunctions = {
CiphertextMessage_Serialize: (obj: Wrapper<CiphertextMessage>) => Uint8Array<ArrayBuffer>;
CiphertextMessage_FromPlaintextContent: (m: Wrapper<PlaintextContent>) => CiphertextMessage;
SessionRecord_ArchiveCurrentState: (sessionRecord: Wrapper<SessionRecord>) => void;
SessionRecord_HasUsableSenderChain: (s: Wrapper<SessionRecord>, now: Timestamp) => boolean;
SessionRecord_HasUsableSenderChain: (s: Wrapper<SessionRecord>, requirePqRatio: number, now: Timestamp) => boolean;
SessionRecord_CurrentRatchetKeyMatches: (s: Wrapper<SessionRecord>, key: Wrapper<PublicKey>) => boolean;
SessionRecord_Deserialize: (data: Uint8Array<ArrayBuffer>) => SessionRecord;
SessionRecord_Serialize: (obj: Wrapper<SessionRecord>) => Uint8Array<ArrayBuffer>;

View File

@ -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 {

View File

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

View File

@ -82,6 +82,7 @@ def translate_to_java(typ: str) -> Tuple[str, bool]:
'Nullable<ObjectHandle>': 'ObjectHandle',
'jint': 'Int',
'jlong': 'Long',
'jdouble': 'Double',
'jboolean': 'Boolean',
'JObject': 'Object',
'JClass': 'Class<*>',

View File

@ -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',

View File

@ -974,8 +974,35 @@ fn SessionRecord_ArchiveCurrentState(session_record: &mut SessionRecord) -> Resu
}
#[bridge_fn]
fn SessionRecord_HasUsableSenderChain(s: &SessionRecord, now: Timestamp) -> Result<bool> {
s.has_usable_sender_chain(now.into(), SessionUsabilityRequirements::NotStale)
fn SessionRecord_HasUsableSenderChain(
s: &SessionRecord,
require_pq_ratio: f64,
now: Timestamp,
) -> Result<bool> {
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]

View File

@ -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>) => (u32);
(usize) => (usize);
(bool) => (bool);
@ -1549,6 +1551,7 @@ macro_rules! ffi_result_type {
(Option<u32>) => (u32);
(u64) => (u64);
(i64) => (i64);
(f64) => (f64);
(Option<u64>) => (u64);
(bool) => (bool);
(&str) => (*const std::ffi::c_char);

View File

@ -239,6 +239,13 @@ impl SimpleArgTypeInfo<'_> for u64 {
}
}
impl SimpleArgTypeInfo<'_> for f64 {
type ArgType = jdouble;
fn convert_from(_env: &mut JNIEnv, foreign: &jdouble) -> Result<Self, BridgeLayerError> {
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
};

View File

@ -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};

View File

@ -359,6 +359,13 @@ impl SimpleArgTypeInfo for u64 {
}
}
impl SimpleArgTypeInfo for f64 {
type ArgType = JsNumber;
fn convert_from(cx: &mut FunctionContext, foreign: Handle<Self::ArgType>) -> NeonResult<Self> {
Ok(foreign.value(cx))
}
}
impl SimpleArgTypeInfo for String {
type ArgType = JsString;
fn convert_from(cx: &mut FunctionContext, foreign: Handle<Self::ArgType>) -> NeonResult<Self> {

View File

@ -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";

View File

@ -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,

View File

@ -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));
}
}

View File

@ -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

View File

@ -39,18 +39,15 @@ public class SessionRecord: ClonableHandleOwner<SignalMutPointerSessionRecord> {
}
}
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),
)
}
}

View File

@ -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);

View File

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