All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage Maven / Gradle / Ivy

There is a newer version: 0.65.2
Show newest version
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//

package org.signal.libsignal.protocol;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.protocol.util.Pair;

/**
 * A parsed Sealed Sender v2 "SentMessage", ready to be fanned out to multiple recipients.
 *
 * 

The implementation assumes that every device for a particular recipient should use the same * key material. */ public class SealedSenderMultiRecipientMessage { private final byte[] fullMessageData; private final Map recipients; private final List excludedRecipients; private final int offsetOfSharedData; /** * Per-recipient data for an SSv2 SentMessage. * * @see #messageForRecipient */ public static class Recipient { private final byte[] devices; private final short[] registrationIds; private final int offsetOfRecipientSpecificKeyMaterial; // This is redundant in the current SSv2 format, but if we didn't have it, we'd be hardcoding // the length of the recipient-specific key material on the Java side. private final int lengthOfRecipientSpecificKeyMaterial; private Recipient( byte[] devices, short[] registrationIds, int offsetOfRecipientSpecificKeyMaterial, int lengthOfRecipientSpecificKeyMaterial) { assert devices.length == registrationIds.length; this.devices = devices; this.registrationIds = registrationIds; this.offsetOfRecipientSpecificKeyMaterial = offsetOfRecipientSpecificKeyMaterial; this.lengthOfRecipientSpecificKeyMaterial = lengthOfRecipientSpecificKeyMaterial; } /** * Returns the devices specified for this recipient. * *

A valid message should never have duplicate devices, but the parsing does not enforce * this. * *

The result is returned by reference; mutate it at your own detriment. */ public byte[] getDevices() { return devices; } /** * Returns a stream of (device ID, registration ID) pairs. * * @see #getDevices */ public Stream> getDevicesAndRegistrationIds() { return IntStream.range(0, devices.length) .mapToObj(i -> new Pair<>(devices[i], registrationIds[i])); } @Override public boolean equals(Object other) { if (!(other instanceof Recipient)) { return false; } Recipient otherRecipient = (Recipient) other; return Arrays.equals(devices, otherRecipient.devices) && Arrays.equals(registrationIds, otherRecipient.registrationIds) && offsetOfRecipientSpecificKeyMaterial == otherRecipient.offsetOfRecipientSpecificKeyMaterial && lengthOfRecipientSpecificKeyMaterial == otherRecipient.lengthOfRecipientSpecificKeyMaterial; } @Override public int hashCode() { // Leave out the length field, since in practice it will always be the same. return offsetOfRecipientSpecificKeyMaterial ^ Arrays.hashCode(devices) ^ Arrays.hashCode(registrationIds); } } /** * Parses the input as an SSv2 SentMessage. * *

The input is kept alive as long as the result is; it's used to implement {@link * #messageForRecipient} and {@link #serialized}. * * @throws InvalidVersionException if the major version of the sealed sender message is * unrecognized * @throws InvalidMessageException if the message is malformed */ public static SealedSenderMultiRecipientMessage parse(byte[] input) throws InvalidMessageException, InvalidVersionException { return (SealedSenderMultiRecipientMessage) Native.SealedSender_MultiRecipientParseSentMessage(input); } private SealedSenderMultiRecipientMessage( byte[] fullMessageData, Map recipients, List excludedRecipients, int offsetOfSharedData) { this.fullMessageData = fullMessageData; this.recipients = recipients; this.excludedRecipients = excludedRecipients; this.offsetOfSharedData = offsetOfSharedData; } /** * Returns the serialized full message data. * *

The result is returned by reference; mutate it at your own detriment. */ public byte[] serialized() { return fullMessageData; } /** * Returns the recipients parsed from the message. * *

The iteration order of the resulting Map is deterministic: the same input message data will * produce the same output even across multiple runs. * *

The result is returned by reference; mutate it at your own detriment. */ public Map getRecipients() { return recipients; } /** * Returns the recipients excluded from receiving the message. * *

This is enforced to be disjoint from the recipients in {@link #getRecipients}; it may be * used for authorization purposes or just to check that certain recipients were deliberately * excluded rather than accidentally. * *

The iteration order is deterministic: the same input message data will produce the same * output even across multiple runs. * *

The result is returned by reference; mutate it at your own detriment. */ public List getExcludedRecipients() { return excludedRecipients; } /** * Returns the Sealed Sender V2 "ReceivedMessage" payload for delivery to a particular recipient. * *

The same payload should be sent to all of the recipient's devices. */ public byte[] messageForRecipient(Recipient recipient) { final int lengthOfSharedData = fullMessageData.length - offsetOfSharedData; final ByteBuffer bbuf = ByteBuffer.allocate(messageSizeForRecipient(recipient)); bbuf.put((byte) 0x22); // The "original" Sealed Sender V2 version bbuf.put( fullMessageData, recipient.offsetOfRecipientSpecificKeyMaterial, recipient.lengthOfRecipientSpecificKeyMaterial); bbuf.put(fullMessageData, offsetOfSharedData, lengthOfSharedData); return bbuf.array(); } /** * Returns the length of the Sealed Sender V2 "ReceivedMessage" payload for delivery to a * particular recipient, without copying any buffers. */ public int messageSizeForRecipient(Recipient recipient) { final int lengthOfSharedData = fullMessageData.length - offsetOfSharedData; return 1 /* version signature */ + recipient.lengthOfRecipientSpecificKeyMaterial + lengthOfSharedData; } private static final byte SERIALIZED_RECIPIENT_VIEW_VERSION = 0x01; private static final byte[] ZERO_DEVICE_IDS = new byte[0]; private static final short[] ZERO_REGISTRATION_IDS = new short[0]; /** * Returns a serialized view for a particular {@link Recipient}. * *

This may be used to optimize temporary storage for future delivery—a single copy of * the serialized message may be stored, and the full recipient list will not need to be reparsed * to deliver to a single recipient. * * @see #serialized() * @see #messageForRecipient(byte[], byte[]) */ public byte[] serializedRecipientView(Recipient recipient) { final ByteBuffer bbuf = ByteBuffer.allocate(1 + 4 + 4 + 4); bbuf.put(SERIALIZED_RECIPIENT_VIEW_VERSION); bbuf.putInt(offsetOfSharedData); bbuf.putInt(recipient.offsetOfRecipientSpecificKeyMaterial); bbuf.putInt(recipient.lengthOfRecipientSpecificKeyMaterial); assert !bbuf.hasRemaining(); return bbuf.array(); } /** * Returns the Sealed Sender V2 "ReceivedMessage" payload for delivery to a particular recipient * from its serialized view. * *

The same payload should be sent to all the recipient's devices. * * @see #serializedRecipientView(Recipient) */ public static byte[] messageForRecipient(byte[] fullMessageData, byte[] serializedRecipientView) throws InvalidVersionException { if (serializedRecipientView.length != 13) { throw new IllegalArgumentException( "Invalid length for serialized view: " + serializedRecipientView.length); } final ByteBuffer wrapped = ByteBuffer.wrap(serializedRecipientView); final byte version = wrapped.get(); if (version != SERIALIZED_RECIPIENT_VIEW_VERSION) { throw new InvalidVersionException("Recipient view version " + version + " is not supported"); } final int offsetOfSharedData = wrapped.getInt(); final int offsetOfRecipientSpecificKeyMaterial = wrapped.getInt(); final int lengthOfRecipientSpecificKeyMaterial = wrapped.getInt(); assert !wrapped.hasRemaining(); // Device, registration, and excluded service IDs aren't used or checked in this path, so we can // use empty arrays. final Recipient recipient = new Recipient( ZERO_DEVICE_IDS, ZERO_REGISTRATION_IDS, offsetOfRecipientSpecificKeyMaterial, lengthOfRecipientSpecificKeyMaterial); final SealedSenderMultiRecipientMessage message = new SealedSenderMultiRecipientMessage( fullMessageData, Collections.emptyMap(), Collections.emptyList(), offsetOfSharedData); return message.messageForRecipient(recipient); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy