mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-15 20:42:00 +00:00
Versioned Profiles support (disabled).
This commit is contained in:
committed by
Greyson Parrelli
parent
f10d1eac61
commit
7ecb50a3fe
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user