0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-20 03:52:17 +02:00

Get registration IDs from sessions for Sealed Sender v2

The app-visible change is that sealedSenderMultiRecipientEncrypt now
takes a SessionStore as well. Sessions will be looked up in bulk using
a new SessionStore API, 'loadExistingSessions' or
'getExistingSessions`. The registration ID is then loaded from each
session and included in the resulting SSv2 payload.

The implementation is a bit of a divergence from some other APIs in
libsignal-client in that the "look up in bulk" step is performed in
the Java, Swift, or TypeScript layer, with the resulting sessions
passed down to Rust. Why? Because otherwise we'd pass a list of
addresses into Rust, which would have to turn them back into a Java,
Swift, or TypeScript array to call the SessionStore method. This would
be (1) a bunch of extra work to implement, and (2) a waste of CPU when
we already /have/ a list of addresses in the correct format: the
argument to sealedSenderMultiRecipientEncrypt.

This is an example of "the boundaries between the Rust and
Java/Swift/TypeScript parts of the library don't have to be perfect;
they're internal to the overall product". In this case, we've taken
that a little further than usual: usually we try to make the
libsignal-protocol API as convenient as possible as well, but here it
had to be a bit lower-level to satisfy the needs of the app language
wrappers. (Specifically, callers need to fetch the list of
SessionRecords themselves.)

P.S. Why doesn't v1 of sealed sender include registration IDs? Because
for SSv1, libsignal-client isn't producing the entire request body to
upload to the server; it's only producing the message content that
will be decrypted by the recipient. With SSv2, the serialized message
the recipient downloads has both shared and per-recipient data in it,
which the server must assemble from the uploaded request. Because of
this, SSv2's encrypt API might as well produce the entire request.
This commit is contained in:
Jordan Rose 2021-05-19 16:26:45 -07:00
parent 92e239c28f
commit 6f9083175e
21 changed files with 221 additions and 22 deletions

View File

@ -178,7 +178,7 @@ public final class Native {
public static native long SealedSessionCipher_DecryptToUsmc(byte[] ctext, IdentityKeyStore identityStore, Object ctx);
public static native byte[] SealedSessionCipher_Encrypt(long destination, long content, IdentityKeyStore identityKeyStore, Object ctx);
public static native byte[] SealedSessionCipher_MultiRecipientEncrypt(long[] recipients, long content, IdentityKeyStore identityKeyStore, Object ctx);
public static native byte[] SealedSessionCipher_MultiRecipientEncrypt(long[] recipients, long[] recipientSessions, long content, IdentityKeyStore identityKeyStore, Object ctx);
public static native byte[] SealedSessionCipher_MultiRecipientMessageForSingleRecipient(byte[] encodedMultiRecipientMessage);
public static native long SenderCertificate_Deserialize(byte[] data);

View File

@ -21,6 +21,7 @@ import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
import org.whispersystems.libsignal.protocol.SenderKeyMessage;
import org.whispersystems.libsignal.protocol.SignalMessage;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
@ -77,16 +78,28 @@ public class SealedSessionCipher {
}
public byte[] multiRecipientEncrypt(List<SignalProtocolAddress> recipients, UnidentifiedSenderMessageContent content)
throws InvalidKeyException, UntrustedIdentityException
throws InvalidKeyException, NoSessionException, UntrustedIdentityException
{
long[] recipientHandles = new long[recipients.size()];
List<SessionRecord> recipientSessions =
this.signalProtocolStore.loadExistingSessions(recipients);
long[] recipientSessionHandles = new long[recipientSessions.size()];
int i = 0;
for (SessionRecord nextSession : recipientSessions) {
recipientSessionHandles[i] = nextSession.nativeHandle();
i++;
}
long[] recipientHandles = new long[recipients.size()];
i = 0;
for (SignalProtocolAddress nextRecipient : recipients) {
recipientHandles[i] = nextRecipient.nativeHandle();
i++;
}
return Native.SealedSessionCipher_MultiRecipientEncrypt(
recipientHandles,
recipientSessionHandles,
content.nativeHandle(),
this.signalProtocolStore,
null);

View File

@ -145,7 +145,7 @@ public class SessionRecord {
theirBaseKey.nativeHandle()));
}
long nativeHandle() {
public long nativeHandle() {
return this.handle;
}
}

View File

@ -5,6 +5,7 @@
*/
package org.whispersystems.libsignal.state;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import java.util.List;
@ -32,6 +33,15 @@ public interface SessionStore {
*/
public SessionRecord loadSession(SignalProtocolAddress address);
/**
* Returns the {@link SessionRecord}s corresponding to the given addresses.
*
* @param addresses The name and device ID of each remote client.
* @return the SessionRecords corresponding to each recipientId + deviceId tuple.
* @throws NoSessionException if any address does not have an active session.
*/
public List<SessionRecord> loadExistingSessions(List<SignalProtocolAddress> addresses) throws NoSessionException;
/**
* Returns all known devices with active sessions for a recipient
*

View File

@ -5,6 +5,7 @@
*/
package org.whispersystems.libsignal.state.impl;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
@ -34,6 +35,23 @@ public class InMemorySessionStore implements SessionStore {
}
}
@Override
public synchronized List<SessionRecord> loadExistingSessions(List<SignalProtocolAddress> addresses) throws NoSessionException {
List<SessionRecord> resultSessions = new LinkedList<>();
for (SignalProtocolAddress remoteAddress : addresses) {
byte[] serialized = sessions.get(remoteAddress);
if (serialized == null) {
throw new NoSessionException("no session for " + remoteAddress);
}
try {
resultSessions.add(new SessionRecord(serialized));
} catch (IOException e) {
throw new AssertionError(e);
}
}
return resultSessions;
}
@Override
public synchronized List<Integer> getSubDeviceSessions(String name) {
List<Integer> deviceIds = new LinkedList<>();

View File

@ -9,6 +9,7 @@ import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.groups.state.InMemorySenderKeyStore;
import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
@ -82,6 +83,11 @@ public class InMemorySignalProtocolStore implements SignalProtocolStore {
return sessionStore.loadSession(address);
}
@Override
public List<SessionRecord> loadExistingSessions(List<SignalProtocolAddress> addresses) throws NoSessionException {
return sessionStore.loadExistingSessions(addresses);
}
@Override
public List<Integer> getSubDeviceSessions(String name) {
return sessionStore.getSubDeviceSessions(name);

2
node/Native.d.ts vendored
View File

@ -99,7 +99,7 @@ export function SealedSenderDecryptionResult_Message(obj: Wrapper<SealedSenderDe
export function SealedSender_DecryptMessage(message: Buffer, trustRoot: Wrapper<PublicKey>, timestamp: number, localE164: string | null, localUuid: string, localDeviceId: number, sessionStore: SessionStore, identityStore: IdentityKeyStore, prekeyStore: PreKeyStore, signedPrekeyStore: SignedPreKeyStore): Promise<SealedSenderDecryptionResult>;
export function SealedSender_DecryptToUsmc(ctext: Buffer, identityStore: IdentityKeyStore, ctx: null): Promise<UnidentifiedSenderMessageContent>;
export function SealedSender_Encrypt(destination: Wrapper<ProtocolAddress>, content: Wrapper<UnidentifiedSenderMessageContent>, identityKeyStore: IdentityKeyStore, ctx: null): Promise<Buffer>;
export function SealedSender_MultiRecipientEncrypt(recipients: Wrapper<ProtocolAddress>[], content: Wrapper<UnidentifiedSenderMessageContent>, identityKeyStore: IdentityKeyStore, ctx: null): Promise<Buffer>;
export function SealedSender_MultiRecipientEncrypt(recipients: Wrapper<ProtocolAddress>[], recipientSessions: Wrapper<SessionRecord>[], content: Wrapper<UnidentifiedSenderMessageContent>, identityKeyStore: IdentityKeyStore, ctx: null): Promise<Buffer>;
export function SealedSender_MultiRecipientMessageForSingleRecipient(encodedMultiRecipientMessage: Buffer): Buffer;
export function SenderCertificate_Deserialize(buffer: Buffer): SenderCertificate;
export function SenderCertificate_GetCertificate(obj: Wrapper<SenderCertificate>): Buffer;

View File

@ -1008,6 +1008,9 @@ export abstract class SessionStore implements Native.SessionStore {
record: SessionRecord
): Promise<void>;
abstract getSession(name: ProtocolAddress): Promise<SessionRecord | null>;
abstract getExistingSessions(
addresses: ProtocolAddress[]
): Promise<SessionRecord[]>;
}
export abstract class IdentityKeyStore implements Native.IdentityKeyStore {
@ -1316,13 +1319,16 @@ export function sealedSenderEncrypt(
return NativeImpl.SealedSender_Encrypt(address, content, identityStore, null);
}
export function sealedSenderMultiRecipientEncrypt(
export async function sealedSenderMultiRecipientEncrypt(
content: UnidentifiedSenderMessageContent,
recipients: ProtocolAddress[],
identityStore: IdentityKeyStore
identityStore: IdentityKeyStore,
sessionStore: SessionStore
): Promise<Buffer> {
return NativeImpl.SealedSender_MultiRecipientEncrypt(
const recipientSessions = await sessionStore.getExistingSessions(recipients);
return await NativeImpl.SealedSender_MultiRecipientEncrypt(
recipients,
recipientSessions,
content,
identityStore,
null

View File

@ -21,7 +21,7 @@ SignalClient.initLogger(
);
class InMemorySessionStore extends SignalClient.SessionStore {
private state = new Map();
private state = new Map<string, Buffer>();
async saveSession(
name: SignalClient.ProtocolAddress,
record: SignalClient.SessionRecord
@ -33,14 +33,27 @@ class InMemorySessionStore extends SignalClient.SessionStore {
name: SignalClient.ProtocolAddress
): Promise<SignalClient.SessionRecord | null> {
const idx = name.name() + '::' + name.deviceId();
if (this.state.has(idx)) {
const serialized = this.state.get(idx);
if (serialized) {
return Promise.resolve(
SignalClient.SessionRecord.deserialize(this.state.get(idx))
SignalClient.SessionRecord.deserialize(serialized)
);
} else {
return Promise.resolve(null);
}
}
async getExistingSessions(
addresses: SignalClient.ProtocolAddress[]
): Promise<SignalClient.SessionRecord[]> {
return addresses.map(address => {
const idx = address.name() + '::' + address.deviceId();
const serialized = this.state.get(idx);
if (!serialized) {
throw 'no session for ' + idx;
}
return SignalClient.SessionRecord.deserialize(serialized);
});
}
}
class InMemoryIdentityKeyStore extends SignalClient.IdentityKeyStore {
@ -1251,7 +1264,8 @@ describe('SignalClient', () => {
const aSealedSenderMessage = await SignalClient.sealedSenderMultiRecipientEncrypt(
aUsmc,
[bAddress],
aKeys
aKeys,
aSess
);
const bSealedSenderMessage = SignalClient.sealedSenderMultiRecipientMessageForSingleRecipient(

View File

@ -49,6 +49,10 @@ def translate_to_ts(typ):
assert(typ.endswith(']'))
return 'Wrapper<' + translate_to_ts(typ[3:-1]) + '>[]'
if typ.startswith('&['):
assert(typ.endswith(']'))
return 'Wrapper<' + translate_to_ts(typ[2:-1]) + '>[]'
if typ.startswith('&'):
return 'Wrapper<' + typ[1:] + '>'

View File

@ -874,6 +874,42 @@ macro_rules! node_bridge_handle {
}
}
}
// FIXME: This is necessarily a cloning API.
// We should try to avoid cloning data when possible.
impl<'storage> node::AsyncArgTypeInfo<'storage>
for &'storage [$typ] {
type ArgType = node::JsArray;
type StoredType = node::DefaultFinalize<Vec<$typ>>;
fn save_async_arg(
cx: &mut node::FunctionContext,
array: node::Handle<Self::ArgType>,
) -> node::NeonResult<Self::StoredType> {
let len = array.len(cx);
let result = (0..len)
.map(|i| {
let element = neon::object::Object::get(*array, cx, i)?;
let wrapper = element.downcast_or_throw::<node::JsObject, _>(cx)?;
let value_box = neon::object::Object::get(
*wrapper,
cx,
node::NATIVE_HANDLE_PROPERTY
)?;
let value_box: node::Handle<node::DefaultJsBox<std::cell::RefCell<$typ>>> =
value_box.downcast_or_throw(cx)?;
let cell: &std::cell::RefCell<_> = &***value_box;
let result = cell.borrow().clone();
Ok(result)
})
.collect::<node::NeonResult<_>>()?;
Ok(node::DefaultFinalize(result))
}
fn load_async_arg(
stored: &'storage mut Self::StoredType,
) -> Self {
&stored.0
}
}
};
( $typ:ty $(, mut = $_:tt)?) => {
paste! {

View File

@ -982,10 +982,11 @@ async fn SealedSessionCipher_Encrypt<E: Env>(
Ok(env.buffer(ctext))
}
#[bridge_fn_buffer(jni = "SealedSessionCipher_1MultiRecipientEncrypt")]
#[bridge_fn_buffer(jni = "SealedSessionCipher_1MultiRecipientEncrypt", node = false)]
async fn SealedSender_MultiRecipientEncrypt<E: Env>(
env: E,
recipients: &[&ProtocolAddress],
recipient_sessions: &[&SessionRecord],
content: &UnidentifiedSenderMessageContent,
identity_key_store: &mut dyn IdentityKeyStore,
ctx: Context,
@ -993,6 +994,30 @@ async fn SealedSender_MultiRecipientEncrypt<E: Env>(
let mut rng = rand::rngs::OsRng;
let ctext = sealed_sender_multi_recipient_encrypt(
recipients,
recipient_sessions,
content,
identity_key_store,
ctx,
&mut rng,
)
.await?;
Ok(env.buffer(ctext))
}
// Node can't support the `&[&Foo]` type, so we clone the sessions instead.
#[bridge_fn_buffer(ffi = false, jni = false, node = "SealedSender_MultiRecipientEncrypt")]
async fn SealedSender_MultiRecipientEncryptNode<E: Env>(
env: E,
recipients: &[&ProtocolAddress],
recipient_sessions: &[SessionRecord],
content: &UnidentifiedSenderMessageContent,
identity_key_store: &mut dyn IdentityKeyStore,
ctx: Context,
) -> Result<E::Buffer> {
let mut rng = rand::rngs::OsRng;
let ctext = sealed_sender_multi_recipient_encrypt(
recipients,
&recipient_sessions.iter().collect::<Vec<&SessionRecord>>(),
content,
identity_key_store,
ctx,

View File

@ -5,8 +5,8 @@
use crate::{
message_encrypt, CiphertextMessageType, Context, Direction, IdentityKeyStore, KeyPair,
PreKeySignalMessage, PreKeyStore, PrivateKey, ProtocolAddress, PublicKey, Result, SessionStore,
SignalMessage, SignalProtocolError, SignedPreKeyStore, HKDF,
PreKeySignalMessage, PreKeyStore, PrivateKey, ProtocolAddress, PublicKey, Result,
SessionRecord, SessionStore, SignalMessage, SignalProtocolError, SignedPreKeyStore, HKDF,
};
use crate::crypto;
@ -870,11 +870,18 @@ mod sealed_sender_v2 {
pub async fn sealed_sender_multi_recipient_encrypt<R: Rng + CryptoRng>(
destinations: &[&ProtocolAddress],
destination_sessions: &[&SessionRecord],
usmc: &UnidentifiedSenderMessageContent,
identity_store: &mut dyn IdentityKeyStore,
ctx: Context,
rng: &mut R,
) -> Result<Vec<u8>> {
if destinations.len() != destination_sessions.len() {
return Err(SignalProtocolError::InvalidArgument(
"must have the same number of destination sessions as addresses".to_string(),
));
}
let m: [u8; 32] = rng.gen();
let keys = sealed_sender_v2::DerivedKeys::calculate(&m);
let e_pub = keys.e.public_key()?;
@ -903,7 +910,7 @@ pub async fn sealed_sender_multi_recipient_encrypt<R: Rng + CryptoRng>(
.expect("cannot fail encoding to Vec");
let our_identity = identity_store.get_identity_key_pair(ctx).await?;
for destination in destinations {
for (destination, session) in destinations.iter().zip(destination_sessions) {
let their_uuid = Uuid::parse_str(destination.name()).map_err(|_| {
SignalProtocolError::InvalidArgument(format!(
"multi-recipient sealed sender requires UUID recipients (not {})",
@ -916,6 +923,17 @@ pub async fn sealed_sender_multi_recipient_encrypt<R: Rng + CryptoRng>(
.await?
.ok_or_else(|| SignalProtocolError::SessionNotFound(format!("{}", destination)))?;
let their_registration_id = session.remote_registration_id()?;
let their_registration_id = u16::try_from(their_registration_id).map_err(|_| {
SignalProtocolError::InvalidState(
"remote_registration_id",
format!(
"{} has too-high registration ID {:#X}",
destination, their_registration_id
),
)
})?;
let c_i = sealed_sender_v2::apply_agreement_xor(
&keys.e,
their_identity.public_key(),
@ -934,8 +952,7 @@ pub async fn sealed_sender_multi_recipient_encrypt<R: Rng + CryptoRng>(
serialized.extend_from_slice(their_uuid.as_bytes());
prost::encode_length_delimiter(destination.device_id() as usize, &mut serialized)
.expect("cannot fail encoding to Vec");
// Provide a placeholder registration ID for now.
serialized.extend_from_slice(&0u16.to_be_bytes());
serialized.extend_from_slice(&their_registration_id.to_be_bytes());
serialized.extend_from_slice(&c_i);
serialized.extend_from_slice(&at_i);
}

View File

@ -195,6 +195,23 @@ impl InMemSessionStore {
sessions: HashMap::new(),
}
}
/// Bulk version of [SessionStore::load_session]
///
/// Useful for [crate::sealed_sender_multi_recipient_encrypt].
pub fn load_existing_sessions(
&self,
addresses: &[&ProtocolAddress],
) -> Result<Vec<&SessionRecord>> {
addresses
.iter()
.map(|address| {
self.sessions
.get(address)
.ok_or_else(|| SignalProtocolError::SessionNotFound(address.to_string()))
})
.collect()
}
}
impl Default for InMemSessionStore {

View File

@ -328,8 +328,12 @@ fn group_sealed_sender() -> Result<(), SignalProtocolError> {
Some([42].to_vec()),
)?;
let recipients = [&bob_uuid_address, &carol_uuid_address];
let alice_ctext = sealed_sender_multi_recipient_encrypt(
&[&bob_uuid_address, &carol_uuid_address],
&recipients,
&alice_store
.session_store
.load_existing_sessions(&recipients)?,
&alice_usmc,
&mut alice_store.identity_store,
None,

View File

@ -496,8 +496,12 @@ fn test_sealed_sender_multi_recipient() -> Result<(), SignalProtocolError> {
None,
)?;
let recipients = [&bob_uuid_address];
let alice_ctext = sealed_sender_multi_recipient_encrypt(
&[&bob_uuid_address],
&recipients,
&alice_store
.session_store
.load_existing_sessions(&recipients)?,
&alice_usmc,
&mut alice_store.identity_store,
None,
@ -546,8 +550,12 @@ fn test_sealed_sender_multi_recipient() -> Result<(), SignalProtocolError> {
None,
)?;
let recipients = [&bob_uuid_address];
let alice_ctext = sealed_sender_multi_recipient_encrypt(
&[&bob_uuid_address],
&recipients,
&alice_store
.session_store
.load_existing_sessions(&recipients)?,
&alice_usmc,
&mut alice_store.identity_store,
None,
@ -602,8 +610,12 @@ fn test_sealed_sender_multi_recipient() -> Result<(), SignalProtocolError> {
None,
)?;
let recipients = [&bob_uuid_address];
let alice_ctext = sealed_sender_multi_recipient_encrypt(
&[&bob_uuid_address],
&recipients,
&alice_store
.session_store
.load_existing_sessions(&recipients)?,
&alice_usmc,
&mut alice_store.identity_store,
None,

View File

@ -94,6 +94,15 @@ public class InMemorySignalProtocolStore: IdentityKeyStore, PreKeyStore, SignedP
return sessionMap[address]
}
public func loadExistingSessions(for addresses: [ProtocolAddress], context: StoreContext) throws -> [SessionRecord] {
return try addresses.map { address in
if let session = sessionMap[address] {
return session
}
throw SignalError.sessionNotFound("\(address)")
}
}
public func storeSession(_ record: SessionRecord, for address: ProtocolAddress, context: StoreContext) throws {
sessionMap[address] = record
}

View File

@ -37,6 +37,7 @@ public protocol SignedPreKeyStore: AnyObject {
public protocol SessionStore: AnyObject {
func loadSession(for address: ProtocolAddress, context: StoreContext) throws -> SessionRecord?
func loadExistingSessions(for addresses: [ProtocolAddress], context: StoreContext) throws -> [SessionRecord]
func storeSession(_ record: SessionRecord, for address: ProtocolAddress, context: StoreContext) throws
}

View File

@ -150,13 +150,17 @@ public func sealedSenderEncrypt(_ content: UnidentifiedSenderMessageContent,
public func sealedSenderMultiRecipientEncrypt(_ content: UnidentifiedSenderMessageContent,
for recipients: [ProtocolAddress],
identityStore: IdentityKeyStore,
sessionStore: SessionStore,
context: StoreContext) throws -> [UInt8] {
let sessions = try sessionStore.loadExistingSessions(for: recipients, context: context)
return try context.withOpaquePointer { context in
try withIdentityKeyStore(identityStore) { ffiIdentityStore in
try invokeFnReturningArray {
signal_sealed_sender_multi_recipient_encrypt($0, $1,
recipients.map { $0.nativeHandle },
recipients.count,
sessions.map { $0.nativeHandle },
sessions.count,
content.nativeHandle,
ffiIdentityStore, context)
}

View File

@ -899,6 +899,8 @@ SignalFfiError *signal_sealed_sender_multi_recipient_encrypt(const unsigned char
size_t *out_len,
const SignalProtocolAddress *const *recipients,
size_t recipients_len,
const SignalSessionRecord *const *recipient_sessions,
size_t recipient_sessions_len,
const SignalUnidentifiedSenderMessageContent *content,
const SignalIdentityKeyStore *identity_key_store,
void *ctx);

View File

@ -283,6 +283,7 @@ class SessionTests: TestCaseBase {
let a_ctext = try! sealedSenderMultiRecipientEncrypt(a_usmc,
for: [bob_address],
identityStore: alice_store,
sessionStore: alice_store,
context: NullContext())
let b_ctext = try! sealedSenderMultiRecipientMessageForSingleRecipient(a_ctext)