SPQR: API changes to allow remote config for archiving non-PQ sessions.
This commit is contained in:
parent
9adf4191f0
commit
2486ffe4e2
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
v0.93.3
|
||||
v0.94.0
|
||||
|
||||
- keytrans: Detect version changes sooner
|
||||
|
||||
- Expose PQ session archiving ratio API
|
||||
|
||||
@ -23,7 +23,7 @@ repositories {
|
||||
}
|
||||
|
||||
allprojects {
|
||||
version = "0.93.3"
|
||||
version = "0.94.0"
|
||||
group = "org.signal"
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
13
node/package-lock.json
generated
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<*>',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user