Versioned Profiles support (disabled).

This commit is contained in:
Alan Evans
2020-02-10 18:40:22 -05:00
committed by Greyson Parrelli
parent f10d1eac61
commit 7ecb50a3fe
67 changed files with 1200 additions and 321 deletions

View File

@@ -35,6 +35,8 @@ dependencies {
api 'com.squareup.okhttp3:okhttp:3.12.1'
implementation 'org.threeten:threetenbp:1.3.6'
api project(':zkgroups')
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:1.7.1'
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.0.0'

View File

@@ -0,0 +1,13 @@
package org.whispersystems.signalservice;
/**
* A location for constants that allows us to turn features on and off at the service level during development.
* After a feature has been launched, the flag should be removed.
*/
public final class FeatureFlags {
/** Zero Knowledge Group functions */
public static final boolean ZK_GROUPS = false;
/** Read and write versioned profile information. */
public static final boolean VERSIONED_PROFILES = ZK_GROUPS && false;
}

View File

@@ -9,6 +9,7 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -17,18 +18,20 @@ import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
@@ -56,6 +59,7 @@ import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -559,19 +563,27 @@ public class SignalServiceAccountManager {
return this.pushServiceSocket.getTurnServerInfo();
}
public void setProfileName(byte[] key, String name)
public void setProfileName(ProfileKey key, String name)
throws IOException
{
if (FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
if (name == null) name = "";
String ciphertextName = Base64.encodeBytesWithoutPadding(new ProfileCipher(key).encryptName(name.getBytes("UTF-8"), ProfileCipher.NAME_PADDED_LENGTH));
String ciphertextName = Base64.encodeBytesWithoutPadding(new ProfileCipher(key).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.NAME_PADDED_LENGTH));
this.pushServiceSocket.setProfileName(ciphertextName);
}
public void setProfileAvatar(byte[] key, StreamDetails avatar)
public void setProfileAvatar(ProfileKey key, StreamDetails avatar)
throws IOException
{
if (FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
ProfileAvatarData profileAvatarData = null;
if (avatar != null) {
@@ -584,6 +596,33 @@ public class SignalServiceAccountManager {
this.pushServiceSocket.setProfileAvatar(profileAvatarData);
}
public void setVersionedProfile(ProfileKey profileKey, String name, StreamDetails avatar)
throws IOException
{
if (!FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
if (name == null) name = "";
byte[] ciphertextName = new ProfileCipher(profileKey).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.NAME_PADDED_LENGTH);
boolean hasAvatar = avatar != null;
ProfileAvatarData profileAvatarData = null;
if (hasAvatar) {
profileAvatarData = new ProfileAvatarData(avatar.getStream(),
ProfileCipherOutputStream.getCiphertextLength(avatar.getLength()),
avatar.getContentType(),
new ProfileCipherOutputStreamFactory(profileKey));
}
this.pushServiceSocket.writeProfile(new SignalServiceProfileWrite(profileKey.getProfileKeyVersion().serialize(),
ciphertextName,
hasAvatar,
profileKey.getCommitment().serialize()),
profileAvatarData);
}
public void setUsername(String username) throws IOException {
this.pushServiceSocket.setUsername(username);
}

View File

@@ -8,11 +8,21 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
import org.signal.zkgroup.profiles.ProfileKeyVersion;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.util.Hex;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
@@ -25,10 +35,10 @@ import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -47,10 +57,15 @@ public class SignalServiceMessagePipe {
private final WebSocketConnection websocket;
private final Optional<CredentialsProvider> credentialsProvider;
private final ClientZkProfileOperations clientZkProfile;
SignalServiceMessagePipe(WebSocketConnection websocket, Optional<CredentialsProvider> credentialsProvider) {
SignalServiceMessagePipe(WebSocketConnection websocket,
Optional<CredentialsProvider> credentialsProvider,
ClientZkProfileOperations clientZkProfile)
{
this.websocket = websocket;
this.credentialsProvider = credentialsProvider;
this.clientZkProfile = clientZkProfile;
this.websocket.connect();
}
@@ -149,7 +164,12 @@ public class SignalServiceMessagePipe {
}
}
public SignalServiceProfile getProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
public ProfileAndCredential getProfile(SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType)
throws IOException
{
try {
List<String> headers = new LinkedList<>();
@@ -157,12 +177,30 @@ public class SignalServiceMessagePipe {
headers.add("Unidentified-Access-Key:" + Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
}
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
.setId(new SecureRandom().nextLong())
.setVerb("GET")
.setPath(String.format("/v1/profile/%s", address.getIdentifier()))
.addAllHeaders(headers)
.build();
Optional<UUID> uuid = address.getUuid();
SecureRandom random = new SecureRandom();
ProfileKeyCredentialRequestContext requestContext = null;
WebSocketRequestMessage.Builder builder = WebSocketRequestMessage.newBuilder()
.setId(random.nextLong())
.setVerb("GET")
.addAllHeaders(headers);
if (FeatureFlags.VERSIONED_PROFILES && requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL && uuid.isPresent() && profileKey.isPresent()) {
ProfileKeyVersion profileKeyIdentifier = profileKey.get().getProfileKeyVersion();
UUID target = uuid.get();
requestContext = clientZkProfile.createProfileKeyCredentialRequestContext(random, target, profileKey.get());
ProfileKeyCredentialRequest request = requestContext.getRequest();
String version = profileKeyIdentifier.serialize();
String credentialRequest = Hex.toStringCondensed(request.serialize());
builder.setPath(String.format("/v1/profile/%s/%s/%s", target, version, credentialRequest));
} else {
builder.setPath(String.format("/v1/profile/%s", address.getIdentifier()));
}
WebSocketRequestMessage requestMessage = builder.build();
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
@@ -170,8 +208,13 @@ public class SignalServiceMessagePipe {
throw new IOException("Non-successful response: " + response.first());
}
return JsonUtil.fromJson(response.second(), SignalServiceProfile.class);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(response.second(), SignalServiceProfile.class);
ProfileKeyCredential profileKeyCredential = requestContext != null && signalServiceProfile.getProfileKeyCredentialResponse() != null
? clientZkProfile.receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
: null;
return new ProfileAndCredential(signalServiceProfile, requestType, Optional.fromNullable(profileKeyCredential));
} catch (InterruptedException | ExecutionException | TimeoutException | VerificationFailedException e) {
throw new IOException(e);
}
}

View File

@@ -6,8 +6,14 @@
package org.whispersystems.signalservice.api;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
@@ -16,6 +22,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
@@ -54,6 +61,7 @@ public class SignalServiceMessageReceiver {
private final String signalAgent;
private final ConnectivityListener connectivityListener;
private final SleepTimer sleepTimer;
private final ClientZkProfileOperations clientZkProfile;
/**
* Construct a SignalServiceMessageReceiver.
@@ -91,6 +99,7 @@ public class SignalServiceMessageReceiver {
this.signalAgent = signalAgent;
this.connectivityListener = listener;
this.sleepTimer = timer;
this.clientZkProfile = new ClientZkProfileOperations(new ServerPublicParams(urls.getZkGroupServerPublicParams()));
}
/**
@@ -110,10 +119,21 @@ public class SignalServiceMessageReceiver {
return retrieveAttachment(pointer, destination, maxSizeBytes, null);
}
public SignalServiceProfile retrieveProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
throws IOException
public ProfileAndCredential retrieveProfile(SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType)
throws IOException, VerificationFailedException
{
return socket.retrieveProfile(address, unidentifiedAccess);
Optional<UUID> uuid = address.getUuid();
if (FeatureFlags.VERSIONED_PROFILES && requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL && uuid.isPresent() && profileKey.isPresent()) {
return socket.retrieveProfile(uuid.get(), profileKey.get(), unidentifiedAccess);
} else {
return new ProfileAndCredential(socket.retrieveProfile(address, unidentifiedAccess),
SignalServiceProfile.RequestType.PROFILE,
Optional.<ProfileKeyCredential>absent());
}
}
public SignalServiceProfile retrieveProfileByUsername(String username, Optional<UnidentifiedAccess> unidentifiedAccess)
@@ -122,7 +142,7 @@ public class SignalServiceMessageReceiver {
return socket.retrieveProfileByUsername(username, unidentifiedAccess);
}
public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes)
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, int maxSizeBytes)
throws IOException
{
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
@@ -203,7 +223,7 @@ public class SignalServiceMessageReceiver {
sleepTimer,
urls.getNetworkInterceptors());
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
}
public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
@@ -213,7 +233,7 @@ public class SignalServiceMessageReceiver {
sleepTimer,
urls.getNetworkInterceptors());
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
}
public List<SignalServiceEnvelope> retrieveMessages() throws IOException {

View File

@@ -1,6 +1,7 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.internal.util.Util;
@@ -21,9 +22,9 @@ public class ProfileCipher {
public static final int NAME_PADDED_LENGTH = 53;
private final byte[] key;
private final ProfileKey key;
public ProfileCipher(byte[] key) {
public ProfileCipher(ProfileKey key) {
this.key = key;
}
@@ -40,7 +41,7 @@ public class ProfileCipher {
byte[] nonce = Util.getSecretBytes(12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
return ByteUtil.combine(nonce, cipher.doFinal(inputPadded));
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
@@ -58,7 +59,7 @@ public class ProfileCipher {
System.arraycopy(input, 0, nonce, 0, nonce.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
byte[] paddedPlaintext = cipher.doFinal(input, nonce.length, input.length - nonce.length);
int plaintextLength = 0;

View File

@@ -1,6 +1,7 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.FilterInputStream;
@@ -24,7 +25,7 @@ public class ProfileCipherInputStream extends FilterInputStream {
private boolean finished = false;
public ProfileCipherInputStream(InputStream in, byte[] key) throws IOException {
public ProfileCipherInputStream(InputStream in, ProfileKey key) throws IOException {
super(in);
try {
@@ -33,7 +34,7 @@ public class ProfileCipherInputStream extends FilterInputStream {
byte[] nonce = new byte[12];
Util.readFully(in, nonce);
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {

View File

@@ -1,5 +1,7 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.zkgroup.profiles.ProfileKey;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
@@ -18,13 +20,13 @@ public class ProfileCipherOutputStream extends DigestingOutputStream {
private final Cipher cipher;
public ProfileCipherOutputStream(OutputStream out, byte[] key) throws IOException {
public ProfileCipherOutputStream(OutputStream out, ProfileKey key) throws IOException {
super(out);
try {
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] nonce = generateNonce();
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
super.write(nonce, 0, nonce.length);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {

View File

@@ -3,6 +3,7 @@ package org.whispersystems.signalservice.api.crypto;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.ByteUtil;
import java.security.InvalidAlgorithmParameterException;
@@ -36,13 +37,13 @@ public class UnidentifiedAccess {
return unidentifiedCertificate;
}
public static byte[] deriveAccessKeyFrom(byte[] profileKey) {
public static byte[] deriveAccessKeyFrom(ProfileKey profileKey) {
try {
byte[] nonce = new byte[12];
byte[] input = new byte[16];
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey, "AES"), new GCMParameterSpec(128, nonce));
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey.serialize(), "AES"), new GCMParameterSpec(128, nonce));
byte[] ciphertext = cipher.doFinal(input);

View File

@@ -6,6 +6,7 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -17,10 +18,10 @@ public class DeviceContact {
private final Optional<SignalServiceAttachmentStream> avatar;
private final Optional<String> color;
private final Optional<VerifiedMessage> verified;
private final Optional<byte[]> profileKey;
private final Optional<ProfileKey> profileKey;
private final boolean blocked;
private final Optional<Integer> expirationTimer;
private final Optional<Integer> inboxPosition;
private final Optional<Integer> inboxPosition;
private final boolean archived;
public DeviceContact(SignalServiceAddress address,
@@ -28,7 +29,7 @@ public class DeviceContact {
Optional<SignalServiceAttachmentStream> avatar,
Optional<String> color,
Optional<VerifiedMessage> verified,
Optional<byte[]> profileKey,
Optional<ProfileKey> profileKey,
boolean blocked,
Optional<Integer> expirationTimer,
Optional<Integer> inboxPosition,
@@ -66,7 +67,7 @@ public class DeviceContact {
return verified;
}
public Optional<byte[]> getProfileKey() {
public Optional<ProfileKey> getProfileKey() {
return profileKey;
}

View File

@@ -6,6 +6,8 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
@@ -44,7 +46,7 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
Optional<String> color = details.hasColor() ? Optional.of(details.getColor()) : Optional.<String>absent();
Optional<VerifiedMessage> verified = Optional.absent();
Optional<byte[]> profileKey = Optional.absent();
Optional<ProfileKey> profileKey = Optional.absent();
boolean blocked = false;
Optional<Integer> expireTimer = Optional.absent();
Optional<Integer> inboxPosition = Optional.absent();
@@ -84,7 +86,11 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
}
if (details.hasProfileKey()) {
profileKey = Optional.fromNullable(details.getProfileKey().toByteArray());
try {
profileKey = Optional.fromNullable(new ProfileKey(details.getProfileKey().toByteArray()));
} catch (InvalidInputException e) {
Log.w(TAG, "Invalid profile key ignored", e);
}
}
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {

View File

@@ -85,7 +85,7 @@ public class DeviceContactsOutputStream extends ChunkedOutputStream {
}
if (contact.getProfileKey().isPresent()) {
contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get()));
contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get().serialize()));
}
if (contact.getExpirationTimer().isPresent()) {

View File

@@ -0,0 +1,32 @@
package org.whispersystems.signalservice.api.profiles;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.util.guava.Optional;
public final class ProfileAndCredential {
private final SignalServiceProfile profile;
private final SignalServiceProfile.RequestType requestType;
private final Optional<ProfileKeyCredential> profileKeyCredential;
public ProfileAndCredential(SignalServiceProfile profile,
SignalServiceProfile.RequestType requestType,
Optional<ProfileKeyCredential> profileKeyCredential)
{
this.profile = profile;
this.requestType = requestType;
this.profileKeyCredential = profileKeyCredential;
}
public SignalServiceProfile getProfile() {
return profile;
}
public SignalServiceProfile.RequestType getRequestType() {
return requestType;
}
public Optional<ProfileKeyCredential> getProfileKeyCredential() {
return profileKeyCredential;
}
}

View File

@@ -1,16 +1,28 @@
package org.whispersystems.signalservice.api.profiles;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.util.UUID;
public class SignalServiceProfile {
public enum RequestType {
PROFILE,
PROFILE_AND_CREDENTIAL
}
private static final String TAG = SignalServiceProfile.class.getSimpleName();
@JsonProperty
private String identityKey;
@@ -37,6 +49,12 @@ public class SignalServiceProfile {
@JsonDeserialize(using = JsonUtil.UuidDeserializer.class)
private UUID uuid;
@JsonProperty
private byte[] credential;
@JsonIgnore
private RequestType requestType;
public SignalServiceProfile() {}
public String getIdentityKey() {
@@ -71,6 +89,14 @@ public class SignalServiceProfile {
return uuid;
}
public RequestType getRequestType() {
return requestType;
}
public void setRequestType(RequestType requestType) {
this.requestType = requestType;
}
public static class Capabilities {
@JsonProperty
private boolean uuid;
@@ -81,4 +107,17 @@ public class SignalServiceProfile {
return uuid;
}
}
public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() {
if (!FeatureFlags.VERSIONED_PROFILES) return null;
if (credential == null) return null;
try {
return new ProfileKeyCredentialResponse(credential);
} catch (InvalidInputException e) {
Log.w(TAG, e);
return null;
}
}
}

View File

@@ -0,0 +1,34 @@
package org.whispersystems.signalservice.api.profiles;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SignalServiceProfileWrite {
@JsonProperty
private String version;
@JsonProperty
private byte[] name;
@JsonProperty
private boolean avatar;
@JsonProperty
private byte[] commitment;
@JsonCreator
public SignalServiceProfileWrite(){
}
public SignalServiceProfileWrite(String version, byte[] name, boolean avatar, byte[] commitment) {
this.version = version;
this.name = name;
this.avatar = avatar;
this.commitment = commitment;
}
public boolean hasAvatar() {
return avatar;
}
}

View File

@@ -2,11 +2,12 @@ package org.whispersystems.signalservice.api.storage;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.util.Arrays;
import java.util.Objects;
public class SignalContactRecord {
public final class SignalContactRecord {
private final byte[] key;
private final SignalServiceAddress address;
@@ -18,7 +19,7 @@ public class SignalContactRecord {
private final boolean blocked;
private final boolean profileSharingEnabled;
private final Optional<String> nickname;
private final int protoVersion;
private final int protoVersion;
private SignalContactRecord(byte[] key,
SignalServiceAddress address,
@@ -42,7 +43,7 @@ public class SignalContactRecord {
this.blocked = blocked;
this.profileSharingEnabled = profileSharingEnabled;
this.nickname = Optional.fromNullable(nickname);
this.protoVersion = protoVersion;
this.protoVersion = protoVersion;
}
public byte[] getKey() {
@@ -98,18 +99,20 @@ public class SignalContactRecord {
profileSharingEnabled == contact.profileSharingEnabled &&
Arrays.equals(key, contact.key) &&
Objects.equals(address, contact.address) &&
Objects.equals(profileName, contact.profileName) &&
Objects.equals(profileKey, contact.profileKey) &&
Objects.equals(username, contact.username) &&
Objects.equals(identityKey, contact.identityKey) &&
profileName.equals(contact.profileName) &&
OptionalUtil.byteArrayEquals(profileKey, contact.profileKey) &&
username.equals(contact.username) &&
OptionalUtil.byteArrayEquals(identityKey, contact.identityKey) &&
identityState == contact.identityState &&
Objects.equals(nickname, contact.nickname);
}
@Override
public int hashCode() {
int result = Objects.hash(address, profileName, profileKey, username, identityKey, identityState, blocked, profileSharingEnabled, nickname);
int result = Objects.hash(address, profileName, username, identityState, blocked, profileSharingEnabled, nickname);
result = 31 * result + Arrays.hashCode(key);
result = 31 * result + OptionalUtil.byteArrayHashCode(profileKey);
result = 31 * result + OptionalUtil.byteArrayHashCode(identityKey);
return result;
}
@@ -138,7 +141,7 @@ public class SignalContactRecord {
}
public Builder setProfileKey(byte[] profileKey) {
this.profileKey= profileKey;
this.profileKey = profileKey;
return this;
}

View File

@@ -46,8 +46,8 @@ public final class SignalStorageModels {
}
public static SignalContactRecord remoteToLocalContactRecord(byte[] storageKey, ContactRecord contact) throws IOException {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(storageKey, address);
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(storageKey, address);
if (contact.hasBlocked()) {
builder.setBlocked(contact.getBlocked());

View File

@@ -0,0 +1,27 @@
package org.whispersystems.signalservice.api.util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Arrays;
public final class OptionalUtil {
private OptionalUtil() {
}
public static boolean byteArrayEquals(Optional<byte[]> a, Optional<byte[]> b) {
if (a.isPresent() != b.isPresent()) {
return false;
}
if (a.isPresent()) {
return Arrays.equals(a.get(), b.get());
}
return true;
}
public static int byteArrayHashCode(Optional<byte[]> bytes) {
return bytes.isPresent() ? Arrays.hashCode(bytes.get()) : 0;
}
}

View File

@@ -1,9 +1,14 @@
package org.whispersystems.signalservice.api.util;
import org.whispersystems.libsignal.logging.Log;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
public class StreamDetails {
public final class StreamDetails implements Closeable {
private static final String TAG = StreamDetails.class.getSimpleName();
private final InputStream stream;
private final String contentType;
@@ -26,4 +31,13 @@ public class StreamDetails {
public long getLength() {
return length;
}
@Override
public void close() {
try {
stream.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
}

View File

@@ -1,11 +1,10 @@
package org.whispersystems.signalservice.internal.configuration;
import java.util.List;
import okhttp3.Interceptor;
public class SignalServiceConfiguration {
public final class SignalServiceConfiguration {
private final SignalServiceUrl[] signalServiceUrls;
private final SignalCdnUrl[] signalCdnUrls;
@@ -13,13 +12,15 @@ public class SignalServiceConfiguration {
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
private final SignalStorageUrl[] signalStorageUrls;
private final List<Interceptor> networkInterceptors;
private final byte[] zkGroupServerPublicParams;
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
SignalCdnUrl[] signalCdnUrls,
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls,
SignalStorageUrl[] signalStorageUrls,
List<Interceptor> networkInterceptors)
List<Interceptor> networkInterceptors,
byte[] zkGroupServerPublicParams)
{
this.signalServiceUrls = signalServiceUrls;
this.signalCdnUrls = signalCdnUrls;
@@ -27,6 +28,7 @@ public class SignalServiceConfiguration {
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
this.signalStorageUrls = signalStorageUrls;
this.networkInterceptors = networkInterceptors;
this.zkGroupServerPublicParams = zkGroupServerPublicParams;
}
public SignalServiceUrl[] getSignalServiceUrls() {
@@ -52,4 +54,8 @@ public class SignalServiceConfiguration {
public List<Interceptor> getNetworkInterceptors() {
return networkInterceptors;
}
public byte[] getZkGroupServerPublicParams() {
return zkGroupServerPublicParams;
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.signalservice.internal.groupsv2;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
public final class ClientZkOperations {
private final ClientZkProfileOperations clientZkProfileOperations;
public ClientZkOperations(ServerPublicParams serverPublicParams) {
clientZkProfileOperations = new ClientZkProfileOperations(serverPublicParams);
}
public ClientZkProfileOperations getProfileOperations() {
return clientZkProfileOperations;
}
}

View File

@@ -4,8 +4,6 @@ package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ProfileAvatarUploadAttributes {
@JsonProperty
private String url;
@JsonProperty
private String key;
@@ -30,10 +28,6 @@ public class ProfileAvatarUploadAttributes {
public ProfileAvatarUploadAttributes() {}
public String getUrl() {
return url;
}
public String getKey() {
return key;
}

View File

@@ -9,6 +9,13 @@ package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
import org.signal.zkgroup.profiles.ProfileKeyVersion;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.logging.Log;
@@ -17,11 +24,14 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
@@ -48,6 +58,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResp
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
@@ -170,6 +181,7 @@ public class PushServiceSocket {
private final CredentialsProvider credentialsProvider;
private final String signalAgent;
private final SecureRandom random;
private final ClientZkOperations clientZkOperations;
public PushServiceSocket(SignalServiceConfiguration signalServiceConfiguration, CredentialsProvider credentialsProvider, String signalAgent) {
this.credentialsProvider = credentialsProvider;
@@ -180,6 +192,7 @@ public class PushServiceSocket {
this.keyBackupServiceClients = createConnectionHolders(signalServiceConfiguration.getSignalKeyBackupServiceUrls(), signalServiceConfiguration.getNetworkInterceptors());
this.storageClients = createConnectionHolders(signalServiceConfiguration.getSignalStorageUrls(), signalServiceConfiguration.getNetworkInterceptors());
this.random = new SecureRandom();
this.clientZkOperations = FeatureFlags.ZK_GROUPS ? new ClientZkOperations(new ServerPublicParams(signalServiceConfiguration.getZkGroupServerPublicParams())) : null;
}
public void requestSmsVerificationCode(boolean androidSmsRetriever, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
@@ -543,6 +556,37 @@ public class PushServiceSocket {
}
}
public ProfileAndCredential retrieveProfile(UUID target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess)
throws NonSuccessfulResponseCodeException, VerificationFailedException
{
if (!FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
try {
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion();
ProfileKeyCredentialRequestContext requestContext = clientZkOperations.getProfileOperations().createProfileKeyCredentialRequestContext(random, target, profileKey);
ProfileKeyCredentialRequest request = requestContext.getRequest();
String version = profileKeyIdentifier.serialize();
String credentialRequest = Hex.toStringCondensed(request.serialize());
String subPath = String.format("%s/%s/%s", target, version, credentialRequest);
String response = makeServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, NO_HEADERS, unidentifiedAccess);
SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(response, SignalServiceProfile.class);
ProfileKeyCredential profileKeyCredential = signalServiceProfile.getProfileKeyCredentialResponse() != null
? clientZkOperations.getProfileOperations().receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
: null;
return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.fromNullable(profileKeyCredential));
} catch (IOException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
}
}
public void retrieveProfileAvatar(String path, File destination, int maxSizeBytes)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
@@ -550,12 +594,20 @@ public class PushServiceSocket {
}
public void setProfileName(String name) throws NonSuccessfulResponseCodeException, PushNetworkException {
if (FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
makeServiceRequest(String.format(PROFILE_PATH, "name/" + (name == null ? "" : URLEncoder.encode(name))), "PUT", "");
}
public void setProfileAvatar(ProfileAvatarData profileAvatar)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
if (FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
String response = makeServiceRequest(String.format(PROFILE_PATH, "form/avatar"), "GET", null);
ProfileAvatarUploadAttributes formAttributes;
@@ -576,6 +628,35 @@ public class PushServiceSocket {
}
}
public void writeProfile(SignalServiceProfileWrite signalServiceProfileWrite, ProfileAvatarData profileAvatar)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
if (!FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
String requestBody = JsonUtil.toJson(signalServiceProfileWrite);
ProfileAvatarUploadAttributes formAttributes;
String response = makeServiceRequest(String.format(PROFILE_PATH, ""), "PUT", requestBody);
if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) {
try {
formAttributes = JsonUtil.fromJson(response, ProfileAvatarUploadAttributes.class);
} catch (IOException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
}
uploadToCdn("", formAttributes.getAcl(), formAttributes.getKey(),
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
formAttributes.getCredential(), formAttributes.getDate(),
formAttributes.getSignature(), profileAvatar.getData(),
profileAvatar.getContentType(), profileAvatar.getDataLength(),
profileAvatar.getOutputStreamFactory(), null, null);
}
}
public void setUsername(String username) throws IOException {
makeServiceRequest(String.format(SET_USERNAME_PATH, username), "PUT", "", NO_HEADERS, new ResponseCodeHandler() {
@Override

View File

@@ -1,6 +1,7 @@
package org.whispersystems.signalservice.internal.push.http;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.crypto.DigestingOutputStream;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
@@ -9,9 +10,9 @@ import java.io.OutputStream;
public class ProfileCipherOutputStreamFactory implements OutputStreamFactory {
private final byte[] key;
private final ProfileKey key;
public ProfileCipherOutputStreamFactory(byte[] key) {
public ProfileCipherOutputStreamFactory(ProfileKey key) {
this.key = key;
}

View File

@@ -4,6 +4,8 @@ package org.whispersystems.signalservice.api.crypto;
import junit.framework.TestCase;
import org.conscrypt.Conscrypt;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream;
@@ -16,8 +18,8 @@ public class ProfileCipherTest extends TestCase {
Security.insertProviderAt(Conscrypt.newProvider(), 1);
}
public void testEncryptDecrypt() throws InvalidCiphertextException {
byte[] key = Util.getSecretBytes(32);
public void testEncryptDecrypt() throws InvalidCiphertextException, InvalidInputException {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
byte[] name = cipher.encryptName("Clement\0Duval".getBytes(), ProfileCipher.NAME_PADDED_LENGTH);
byte[] plaintext = cipher.decryptName(name);
@@ -25,7 +27,7 @@ public class ProfileCipherTest extends TestCase {
}
public void testEmpty() throws Exception {
byte[] key = Util.getSecretBytes(32);
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
byte[] name = cipher.encryptName("".getBytes(), 26);
byte[] plaintext = cipher.decryptName(name);
@@ -34,7 +36,7 @@ public class ProfileCipherTest extends TestCase {
}
public void testStreams() throws Exception {
byte[] key = Util.getSecretBytes(32);
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ProfileCipherOutputStream out = new ProfileCipherOutputStream(baos, key);

View File

@@ -3,6 +3,8 @@ package org.whispersystems.signalservice.api.crypto;
import junit.framework.TestCase;
import org.conscrypt.OpenSSLProvider;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import java.security.Security;
import java.util.Arrays;
@@ -15,11 +17,11 @@ public class UnidentifiedAccessTest extends TestCase {
private final byte[] EXPECTED_RESULT = {(byte)0x5a, (byte)0x72, (byte)0x3a, (byte)0xce, (byte)0xe5, (byte)0x2c, (byte)0x5e, (byte)0xa0, (byte)0x2b, (byte)0x92, (byte)0xa3, (byte)0xa3, (byte)0x60, (byte)0xc0, (byte)0x95, (byte)0x95};
public void testKeyDerivation() {
public void testKeyDerivation() throws InvalidInputException {
byte[] key = new byte[32];
Arrays.fill(key, (byte)0x02);
byte[] result = UnidentifiedAccess.deriveAccessKeyFrom(key);
byte[] result = UnidentifiedAccess.deriveAccessKeyFrom(new ProfileKey(key));
assertTrue(Arrays.equals(result, EXPECTED_RESULT));
}

View File

@@ -0,0 +1,53 @@
package org.whispersystems.signalservice.api.util;
import org.junit.Test;
import org.whispersystems.libsignal.util.guava.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public final class OptionalUtilTest {
@Test
public void absent_are_equal() {
assertTrue(OptionalUtil.byteArrayEquals(Optional.<byte[]>absent(), Optional.<byte[]>absent()));
}
@Test
public void first_non_absent_not_equal() {
assertFalse(OptionalUtil.byteArrayEquals(Optional.of(new byte[1]), Optional.<byte[]>absent()));
}
@Test
public void second_non_absent_not_equal() {
assertFalse(OptionalUtil.byteArrayEquals(Optional.<byte[]>absent(), Optional.of(new byte[1])));
}
@Test
public void equal_contents() {
byte[] contentsA = new byte[]{1, 2, 3};
byte[] contentsB = contentsA.clone();
Optional<byte[]> a = Optional.of(contentsA);
Optional<byte[]> b = Optional.of(contentsB);
assertTrue(OptionalUtil.byteArrayEquals(a, b));
assertEquals(OptionalUtil.byteArrayHashCode(a), OptionalUtil.byteArrayHashCode(b));
}
@Test
public void in_equal_contents() {
byte[] contentsA = new byte[]{1, 2, 3};
byte[] contentsB = new byte[]{4, 5, 6};
Optional<byte[]> a = Optional.of(contentsA);
Optional<byte[]> b = Optional.of(contentsB);
assertFalse(OptionalUtil.byteArrayEquals(a, b));
assertNotEquals(OptionalUtil.byteArrayHashCode(a), OptionalUtil.byteArrayHashCode(b));
}
@Test
public void hash_code_absent() {
assertEquals(0, OptionalUtil.byteArrayHashCode(Optional.<byte[]>absent()));
}
}