Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Add support for provisioning and basic synchronisation #21

Closed
wants to merge 9 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,39 @@
package org.whispersystems.signalservice.api;


import com.google.protobuf.ByteString;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;

import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPrivateKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
import org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisionMessage;
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.util.Base64;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;

import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisionMessage;
import com.google.protobuf.ByteString;

/**
* The main interface for creating, registering, and
Expand All @@ -45,10 +49,29 @@
*/
public class SignalServiceAccountManager {

private final PushServiceSocket pushServiceSocket;
private final String user;
private final String userAgent;
private final DynamicCredentialsProvider credentialsProvider;
private final PushServiceSocket pushServiceSocket;
private final ProvisioningSocket provisioningSocket;

/**
* Construct a SignalServiceAccountManager.
*
* @param url The URL for the Signal Service.
* @param trustStore The {@link org.whispersystems.signalservice.api.push.TrustStore} for the SignalService server's TLS certificate.
* @param user A Signal Service phone number.
* @param password A Signal Service password.
* @param deviceId A integer which is provided by the server while linking.
* @param userAgent A string which identifies the client software.
*/
public SignalServiceAccountManager(String url, TrustStore trustStore,
String user, String password, int deviceId,
String userAgent)
{
this.credentialsProvider = new DynamicCredentialsProvider(user, password, null, deviceId);
this.provisioningSocket = new ProvisioningSocket(url, trustStore, userAgent);
this.pushServiceSocket = new PushServiceSocket(url, trustStore, credentialsProvider, userAgent);
}

/**
* Construct a SignalServiceAccountManager.
*
Expand All @@ -62,9 +85,7 @@ public SignalServiceAccountManager(String url, TrustStore trustStore,
String user, String password,
String userAgent)
{
this.pushServiceSocket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, null), userAgent);
this.user = user;
this.userAgent = userAgent;
this(url, trustStore, user, password, SignalServiceAddress.DEFAULT_DEVICE_ID, userAgent);
}

/**
Expand Down Expand Up @@ -114,14 +135,16 @@ public void requestVoiceVerificationCode() throws IOException {
* same install, but probabilistically differ across registrations
* for separate installs.
* @param voice A boolean that indicates whether the client supports secure voice (RedPhone) calls.
* @param fetchesMessages A boolean that indicates whether the client fetches messages instead of relying on GCM
*
* @throws IOException
*/
public void verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean voice)
public void verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId,
boolean voice, boolean fetchesMessages)
throws IOException
{
this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
signalProtocolRegistrationId, voice);
signalProtocolRegistrationId, voice, fetchesMessages);
}

/**
Expand Down Expand Up @@ -248,10 +271,53 @@ public List<ContactTokenDetails> getContacts(Set<String> e164numbers)
public String getAccountVerificationToken() throws IOException {
return this.pushServiceSocket.getAccountVerificationToken();
}

/**
* Request a UUID from the server for linking as a new device.
* Called by the new device.
* @return The UUID, Base64 encoded
* @throws TimeoutException
* @throws IOException
*/
public String getNewDeviceUuid() throws TimeoutException, IOException {
return provisioningSocket.getProvisioningUuid().getUuid();
}

/**
* Request a Code for verification of a new device.
* Called by an already verified device.
* @return An verification code. String of 6 digits
* @throws IOException
*/
public String getNewDeviceVerificationCode() throws IOException {
return this.pushServiceSocket.getNewDeviceVerificationCode();
}

/**
* Finishes a registration as a new device. Called by the new device.<br>
* This method blocks until the already verified device has verified this device.
* @param tempIdentity A temporary identity. Must be the same as the one given to the already verified device.
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key, concatenated.
* @param supportsSms A boolean which indicates whether this device can receive SMS to the account's number.
* @param fetchesMessages A boolean which indicates whether this device fetches messages.
* @param registrationId A random integer generated at install time.
* @param deviceName A name for this device, not its user agent.
* @return Contains the account's permanent IdentityKeyPair and it's number along the deviceId given by the server.
* @throws TimeoutException
* @throws IOException
* @throws InvalidKeyException
*/
public NewDeviceRegistrationReturn finishNewDeviceRegistration(IdentityKeyPair tempIdentity, String signalingKey, boolean supportsSms, boolean fetchesMessages, int registrationId, String deviceName) throws TimeoutException, IOException, InvalidKeyException {
ProvisionMessage msg = provisioningSocket.getProvisioningMessage(tempIdentity);
credentialsProvider.setUser(msg.getNumber());
String provisioningCode = msg.getProvisioningCode();
ECPublicKey publicKey = Curve.decodePoint(msg.getIdentityKeyPublic().toByteArray(), 0);
ECPrivateKey privateKey = Curve.decodePrivatePoint(msg.getIdentityKeyPrivate().toByteArray());
IdentityKeyPair identity = new IdentityKeyPair(new IdentityKey(publicKey), privateKey);
int deviceId = this.pushServiceSocket.finishNewDeviceRegistration(provisioningCode, signalingKey, supportsSms, fetchesMessages, registrationId, deviceName);
credentialsProvider.setDeviceId(deviceId);
return new NewDeviceRegistrationReturn(identity, deviceId, msg.getNumber());
}

public void addDevice(String deviceIdentifier,
ECPublicKey deviceKey,
Expand All @@ -263,7 +329,7 @@ public void addDevice(String deviceIdentifier,
ProvisionMessage message = ProvisionMessage.newBuilder()
.setIdentityKeyPublic(ByteString.copyFrom(identityKeyPair.getPublicKey().serialize()))
.setIdentityKeyPrivate(ByteString.copyFrom(identityKeyPair.getPrivateKey().serialize()))
.setNumber(user)
.setNumber(credentialsProvider.getUser())
.setProvisioningCode(code)
.build();

Expand Down Expand Up @@ -301,5 +367,41 @@ private Map<String, String> createDirectoryServerTokenMap(Collection<String> e16

return tokenMap;
}

/**
* Helper class for holding the returns of finishNewDeviceRegistration()
*/
public class NewDeviceRegistrationReturn {
private final IdentityKeyPair identity;
private final int deviceId;
private final String number;

NewDeviceRegistrationReturn(IdentityKeyPair identity, int deviceId, String number) {
this.identity = identity;
this.deviceId = deviceId;
this.number = number;
}

/**
* @return The account's permanent IdentityKeyPair
*/
public IdentityKeyPair getIdentity() {
return identity;
}

/**
* @return The deviceId for this device given by the server
*/
public int getDeviceId() {
return deviceId;
}

/**
* @return The account's number
*/
public String getNumber() {
return number;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
Expand All @@ -38,6 +39,24 @@ public class SignalServiceMessageReceiver {
private final CredentialsProvider credentialsProvider;
private final String userAgent;

/**
* Construct a SignalServiceMessageReceiver.
*
* @param url The URL of the Signal Service.
* @param trustStore The {@link org.whispersystems.signalservice.api.push.TrustStore} containing
* the server's TLS signing certificate.
* @param user The Signal Service username (eg. phone number).
* @param password The Signal Service user password.
* @param deviceId A integer which is provided by the server while linking.
* @param signalingKey The 52 byte signaling key assigned to this user at registration.
*/
public SignalServiceMessageReceiver(String url, TrustStore trustStore,
String user, String password, int deviceId,
String signalingKey, String userAgent)
{
this(url, trustStore, new StaticCredentialsProvider(user, password, signalingKey, deviceId), userAgent);
}

/**
* Construct a SignalServiceMessageReceiver.
*
Expand All @@ -52,7 +71,7 @@ public SignalServiceMessageReceiver(String url, TrustStore trustStore,
String user, String password,
String signalingKey, String userAgent)
{
this(url, trustStore, new StaticCredentialsProvider(user, password, signalingKey), userAgent);
this(url, trustStore, new StaticCredentialsProvider(user, password, signalingKey, SignalServiceAddress.DEFAULT_DEVICE_ID), userAgent);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
*/
package org.whispersystems.signalservice.api;

import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SessionBuilder;
Expand All @@ -23,13 +24,15 @@
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.push.MismatchedDevices;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
Expand All @@ -47,9 +50,8 @@
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;

/**
* The main interface for sending Signal Service messages.
Expand All @@ -64,7 +66,33 @@ public class SignalServiceMessageSender {
private final SignalProtocolStore store;
private final SignalServiceAddress localAddress;
private final Optional<EventListener> eventListener;
private final CredentialsProvider credentialsProvider;

/**
* Construct a SignalServiceMessageSender.
*
* @param url The URL of the Signal Service.
* @param trustStore The trust store containing the Signal Service's signing TLS certificate.
* @param user The Signal Service username (eg phone number).
* @param password The Signal Service user password.
* @param deviceId A integer which is provided by the server while linking.
* @param store The SignalProtocolStore.
* @param eventListener An optional event listener, which fires whenever sessions are
* setup or torn down for a recipient.
*/
public SignalServiceMessageSender(String url, TrustStore trustStore,
String user, String password, int deviceId,
SignalProtocolStore store,
String userAgent,
Optional<EventListener> eventListener)
{
this.credentialsProvider = new StaticCredentialsProvider(user, password, null, deviceId);
this.socket = new PushServiceSocket(url, trustStore, credentialsProvider, userAgent);
this.store = store;
this.localAddress = new SignalServiceAddress(user);
this.eventListener = eventListener;
}

/**
* Construct a SignalServiceMessageSender.
*
Expand All @@ -82,7 +110,8 @@ public SignalServiceMessageSender(String url, TrustStore trustStore,
String userAgent,
Optional<EventListener> eventListener)
{
this.socket = new PushServiceSocket(url, trustStore, new StaticCredentialsProvider(user, password, null), userAgent);
this.credentialsProvider = new StaticCredentialsProvider(user, password, null, SignalServiceAddress.DEFAULT_DEVICE_ID);
this.socket = new PushServiceSocket(url, trustStore, credentialsProvider, userAgent);
this.store = store;
this.localAddress = new SignalServiceAddress(user);
this.eventListener = eventListener;
Expand Down Expand Up @@ -165,6 +194,8 @@ public void sendMessage(SignalServiceSyncMessage message)
content = createMultiDeviceGroupsContent(message.getGroups().get().asStream());
} else if (message.getRead().isPresent()) {
content = createMultiDeviceReadContent(message.getRead().get());
} else if (message.getRequest().isPresent()) {
content = createRequestContent(message.getRequest().get());
} else if (message.getBlockedList().isPresent()) {
content = createMultiDeviceBlockedContent(message.getBlockedList().get());
} else {
Expand Down Expand Up @@ -261,6 +292,15 @@ private byte[] createMultiDeviceReadContent(List<ReadMessage> readMessages) {

return container.setSyncMessage(builder).build().toByteArray();
}

private byte[] createRequestContent(RequestMessage request) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = SyncMessage.newBuilder();

builder.setRequest(request.getRequest());

return container.setSyncMessage(builder).build().toByteArray();
}

private byte[] createMultiDeviceBlockedContent(BlockedListMessage blocked) {
Content.Builder container = Content.newBuilder();
Expand Down Expand Up @@ -398,12 +438,15 @@ private OutgoingPushMessageList getEncryptedMessages(PushServiceSocket socket,
{
List<OutgoingPushMessage> messages = new LinkedList<>();

if (!recipient.equals(localAddress)) {
boolean myself = recipient.equals(localAddress);
if (!myself || credentialsProvider.getDeviceId() != SignalServiceAddress.DEFAULT_DEVICE_ID) {
messages.add(getEncryptedMessage(socket, recipient, SignalServiceAddress.DEFAULT_DEVICE_ID, plaintext, legacy));
}

for (int deviceId : store.getSubDeviceSessions(recipient.getNumber())) {
messages.add(getEncryptedMessage(socket, recipient, deviceId, plaintext, legacy));
if(!myself || deviceId != credentialsProvider.getDeviceId()) {
messages.add(getEncryptedMessage(socket, recipient, deviceId, plaintext, legacy));
}
}

return new OutgoingPushMessageList(recipient.getNumber(), timestamp, recipient.getRelay().orNull(), messages);
Expand Down
Loading