diff --git a/app/build.gradle b/app/build.gradle
index 41c82319ab..364335b9b7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -230,11 +230,14 @@ android {
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
+ buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\""
buildConfigField "boolean", "DEV_BUILD", "false"
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
+ buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"f2e2a5004794a6c1bac5c4949eadbc243dd02e02d1a93f10fe24584fb70815d8\""
+ buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"f51f435802ada769e67aaf5744372bb7e7d519eecf996d335eb5b46b872b5789\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
@@ -301,7 +304,9 @@ android {
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
+ buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
+ buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"b5a865941f95887018c86725cc92308d34a3084dc2b4e7bd2de5e5e1690b50c6\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
}
release {
diff --git a/libsignal/service/build.gradle b/libsignal/service/build.gradle
index 434f17e637..6be238d254 100644
--- a/libsignal/service/build.gradle
+++ b/libsignal/service/build.gradle
@@ -35,7 +35,7 @@ dependencies {
api 'com.squareup.okhttp3:okhttp:3.12.1'
implementation 'org.threeten:threetenbp:1.3.6'
- testImplementation 'junit:junit:3.8.2'
+ testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:1.7.1'
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.0.0'
}
diff --git a/libsignal/service/protobuf/KeyBackupService.proto b/libsignal/service/protobuf/KeyBackupService.proto
new file mode 100644
index 0000000000..a339fda9fc
--- /dev/null
+++ b/libsignal/service/protobuf/KeyBackupService.proto
@@ -0,0 +1,75 @@
+/**
+ * Copyright (C) 2019 Open Whisper Systems
+ *
+ * Licensed according to the LICENSE file in this repository.
+ */
+syntax = "proto2";
+
+package textsecure;
+
+option java_package = "org.whispersystems.signalservice.internal.keybackup.protos";
+option java_multiple_files = true;
+
+message Request {
+ optional BackupRequest backup = 1;
+ optional RestoreRequest restore = 2;
+ optional DeleteRequest delete = 3;
+}
+
+message Response {
+ optional BackupResponse backup = 1;
+ optional RestoreResponse restore = 2;
+ optional DeleteResponse delete = 3;
+}
+
+message BackupRequest {
+ optional bytes service_id = 1;
+ optional bytes backup_id = 2;
+ optional bytes token = 3;
+ optional uint64 valid_from = 4;
+ optional bytes data = 5;
+ optional bytes pin = 6;
+ optional uint32 tries = 7;
+}
+
+message BackupResponse {
+ enum Status {
+ OK = 1;
+ ALREADY_EXISTS = 2;
+ NOT_YET_VALID = 3;
+ }
+
+ optional Status status = 1;
+ optional bytes token = 2;
+}
+
+message RestoreRequest {
+ optional bytes service_id = 1;
+ optional bytes backup_id = 2;
+ optional bytes token = 3;
+ optional uint64 valid_from = 4;
+ optional bytes pin = 5;
+}
+
+message RestoreResponse {
+ enum Status {
+ OK = 1;
+ TOKEN_MISMATCH = 2;
+ NOT_YET_VALID = 3;
+ MISSING = 4;
+ PIN_MISMATCH = 5;
+ }
+
+ optional Status status = 1;
+ optional bytes token = 2;
+ optional bytes data = 3;
+ optional uint32 tries = 4;
+}
+
+message DeleteRequest {
+ optional bytes service_id = 1;
+ optional bytes backup_id = 2;
+}
+
+message DeleteResponse {
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java
new file mode 100644
index 0000000000..58ad5096a1
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java
@@ -0,0 +1,281 @@
+package org.whispersystems.signalservice.api;
+
+import org.whispersystems.libsignal.logging.Log;
+import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
+import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher;
+import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
+import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
+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.keybackup.protos.BackupResponse;
+import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
+import org.whispersystems.signalservice.internal.push.PushServiceSocket;
+import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
+import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
+import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
+import org.whispersystems.signalservice.internal.util.Hex;
+import org.whispersystems.signalservice.internal.util.Util;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.SignatureException;
+import java.util.Locale;
+
+public final class KeyBackupService {
+
+ private static final String TAG = KeyBackupService.class.getSimpleName();
+
+ private final KeyStore iasKeyStore;
+ private final String enclaveName;
+ private final String mrenclave;
+ private final PushServiceSocket pushServiceSocket;
+ private final int maxTries;
+
+ KeyBackupService(KeyStore iasKeyStore,
+ String enclaveName,
+ String mrenclave,
+ PushServiceSocket pushServiceSocket,
+ int maxTries)
+ {
+ this.iasKeyStore = iasKeyStore;
+ this.enclaveName = enclaveName;
+ this.mrenclave = mrenclave;
+ this.pushServiceSocket = pushServiceSocket;
+ this.maxTries = maxTries;
+ }
+
+ /**
+ * Use this if you don't want to validate that the server has not changed since you last set the pin.
+ */
+ public PinChangeSession newPinChangeSession()
+ throws IOException
+ {
+ return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), null);
+ }
+
+ /**
+ * Use this if you want to validate that the server has not changed since you last set the pin.
+ * The supplied token will have to match for the change to be successful.
+ */
+ public PinChangeSession newPinChangeSession(TokenResponse currentToken)
+ throws IOException
+ {
+ return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
+ }
+
+ /**
+ * Use this to validate that the pin is still set on the server with the current token.
+ * Additionally this validates that no one has used any tries.
+ */
+ public RestoreSession newRestoreSession(TokenResponse currentToken)
+ throws IOException
+ {
+ return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
+ }
+
+ /**
+ * Only call before registration, to see how many tries are left.
+ *
+ * Pass the token to the newRegistrationSession.
+ */
+ public TokenResponse getToken(String authAuthorization) throws IOException {
+ return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
+ }
+
+ /**
+ * Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt.
+ *
+ * @param tokenResponse Supplying a token response from a failed previous attempt prevents certain attacks.
+ */
+ public RestoreSession newRegistrationSession(String authAuthorization, TokenResponse tokenResponse)
+ throws IOException
+ {
+ return newSession(authAuthorization, tokenResponse);
+ }
+
+ private Session newSession(String authorization, TokenResponse currentToken)
+ throws IOException
+ {
+ TokenResponse token = currentToken != null ? currentToken : pushServiceSocket.getKeyBackupServiceToken(authorization, enclaveName);
+
+ return new Session(authorization, token);
+ }
+
+ private class Session implements RestoreSession, PinChangeSession {
+
+ private final String authorization;
+ private final TokenResponse currentToken;
+
+ Session(String authorization, TokenResponse currentToken) {
+ this.authorization = authorization;
+ this.currentToken = currentToken;
+ }
+
+ @Override
+ public RegistrationLockData restorePin(String pin)
+ throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException
+ {
+ int attempt = 0;
+ SecureRandom random = new SecureRandom();
+ TokenResponse token = currentToken;
+
+ while (true) {
+
+ attempt++;
+
+ try {
+ return restorePin(pin, token);
+ } catch (TokenException tokenException) {
+
+ token = tokenException.getToken();
+
+ if (tokenException instanceof KeyBackupServicePinException) {
+ throw (KeyBackupServicePinException) tokenException;
+ }
+
+ if (tokenException.isCanAutomaticallyRetry() && attempt < 5) {
+ // back off randomly, between 250 and 8000 ms
+ int backoffMs = 250 * (1 << (attempt - 1));
+
+ Util.sleep(backoffMs + random.nextInt(backoffMs));
+ } else {
+ throw new UnauthenticatedResponseException("Token mismatch, expended all automatic retries");
+ }
+ }
+ }
+ }
+
+ private RegistrationLockData restorePin(String pin, TokenResponse token)
+ throws UnauthenticatedResponseException, IOException, TokenException, InvalidPinException
+ {
+ PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
+
+ try {
+ final int remainingTries = token.getTries();
+ final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
+ final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(stretchedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName));
+ final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
+ final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation);
+
+ TokenResponse nextToken = status.hasToken()
+ ? new TokenResponse(token.getBackupId(), status.getToken().toByteArray(), status.getTries())
+ : token;
+
+ Log.i(TAG, "Restore " + status.getStatus());
+ switch (status.getStatus()) {
+ case OK:
+ Log.i(TAG, String.format(Locale.US,"Restore OK! data: %s tries: %d", Hex.toStringCondensed(status.getData().toByteArray()), status.getTries()));
+ PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(status.getData().toByteArray());
+ return new RegistrationLockData(masterKey, nextToken);
+ case PIN_MISMATCH:
+ Log.i(TAG, "Restore PIN_MISMATCH");
+ throw new KeyBackupServicePinException(nextToken);
+ case TOKEN_MISMATCH:
+ Log.i(TAG, "Restore TOKEN_MISMATCH");
+ // if the number of tries has not fallen, the pin is correct we're just using an out of date token
+ boolean canRetry = remainingTries == status.getTries();
+ Log.i(TAG, String.format(Locale.US, "Token MISMATCH %d %d", remainingTries, status.getTries()));
+ Log.i(TAG, String.format("Last token %s", Hex.toStringCondensed(token.getToken())));
+ Log.i(TAG, String.format("Next token %s", Hex.toStringCondensed(nextToken.getToken())));
+ throw new TokenException(nextToken, canRetry);
+ case MISSING:
+ Log.i(TAG, "Restore OK! No data though");
+ return null;
+ case NOT_YET_VALID:
+ throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
+ }
+ } catch (InvalidCiphertextException e) {
+ throw new UnauthenticatedResponseException(e);
+ }
+ return null;
+ }
+
+ private RemoteAttestation getAndVerifyRemoteAttestation() throws UnauthenticatedResponseException, IOException {
+ try {
+ return RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.KeyBackup, iasKeyStore, enclaveName, mrenclave, authorization);
+ } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | InvalidCiphertextException | SignatureException e) {
+ throw new UnauthenticatedResponseException(e);
+ }
+ }
+
+ @Override
+ public RegistrationLockData setPin(String pin) throws IOException, UnauthenticatedResponseException, InvalidPinException {
+ PinStretcher.MasterKey masterKey = PinStretcher.stretchPin(pin)
+ .withNewSecurePinKey2();
+
+ TokenResponse tokenResponse = putKbsData(masterKey.getKbsAccessKey(),
+ masterKey.getPinKey2(),
+ enclaveName,
+ currentToken);
+
+ pushServiceSocket.setRegistrationLock(masterKey.getRegistrationLock());
+
+ return new RegistrationLockData(masterKey, tokenResponse);
+ }
+
+ @Override
+ public void removePin() throws IOException, UnauthenticatedResponseException {
+ deleteKbsData();
+
+ pushServiceSocket.removePinV2();
+ }
+
+ private TokenResponse putKbsData(byte[] kbsAccessKey, byte[] kbsData, String enclaveName, TokenResponse token)
+ throws IOException, UnauthenticatedResponseException
+ {
+ try {
+ RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
+ KeyBackupRequest request = KeyBackupCipher.createKeyBackupRequest(kbsAccessKey, kbsData, token, remoteAttestation, Hex.fromStringCondensed(enclaveName), maxTries);
+ KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
+ BackupResponse backupResponse = KeyBackupCipher.getKeyBackupResponse(response, remoteAttestation);
+ BackupResponse.Status status = backupResponse.getStatus();
+
+ switch (status) {
+ case OK:
+ return backupResponse.hasToken() ? new TokenResponse(token.getBackupId(), backupResponse.getToken().toByteArray(), maxTries) : token;
+ case ALREADY_EXISTS:
+ throw new UnauthenticatedResponseException("Already exists");
+ case NOT_YET_VALID:
+ throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
+ default:
+ throw new AssertionError("Unknown response status " + status);
+ }
+ } catch (InvalidCiphertextException e) {
+ throw new UnauthenticatedResponseException(e);
+ }
+ }
+
+ private void deleteKbsData()
+ throws IOException, UnauthenticatedResponseException
+ {
+ try {
+ RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
+ KeyBackupRequest request = KeyBackupCipher.createKeyDeleteRequest(currentToken, remoteAttestation, Hex.fromStringCondensed(enclaveName));
+ KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
+
+ KeyBackupCipher.getKeyDeleteResponseStatus(response, remoteAttestation);
+ } catch (InvalidCiphertextException e) {
+ throw new UnauthenticatedResponseException(e);
+ }
+ }
+ }
+
+ public interface RestoreSession {
+
+ RegistrationLockData restorePin(String pin)
+ throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException;
+ }
+
+ public interface PinChangeSession {
+
+ RegistrationLockData setPin(String pin)
+ throws IOException, UnauthenticatedResponseException, InvalidPinException;
+
+ void removePin()
+ throws IOException, UnauthenticatedResponseException;
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupServicePinException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupServicePinException.java
new file mode 100644
index 0000000000..79668ebde6
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupServicePinException.java
@@ -0,0 +1,17 @@
+package org.whispersystems.signalservice.api;
+
+import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
+
+public final class KeyBackupServicePinException extends TokenException {
+
+ private final int triesRemaining;
+
+ public KeyBackupServicePinException(TokenResponse nextToken) {
+ super(nextToken, false);
+ this.triesRemaining = nextToken.getTries();
+ }
+
+ public int getTriesRemaining() {
+ return triesRemaining;
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java
new file mode 100644
index 0000000000..741a51651f
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java
@@ -0,0 +1,27 @@
+package org.whispersystems.signalservice.api;
+
+import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
+import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
+
+public final class RegistrationLockData {
+
+ private final PinStretcher.MasterKey masterKey;
+ private final TokenResponse tokenResponse;
+
+ RegistrationLockData(PinStretcher.MasterKey masterKey, TokenResponse tokenResponse) {
+ this.masterKey = masterKey;
+ this.tokenResponse = tokenResponse;
+ }
+
+ public PinStretcher.MasterKey getMasterKey() {
+ return masterKey;
+ }
+
+ public TokenResponse getTokenResponse() {
+ return tokenResponse;
+ }
+
+ public int getRemainingTries() {
+ return tokenResponse.getTries();
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java
index 873d23ad1f..d6606efbb4 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java
@@ -9,8 +9,6 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
-import org.whispersystems.curve25519.Curve25519;
-import org.whispersystems.curve25519.Curve25519KeyPair;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -18,7 +16,6 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.logging.Log;
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.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
@@ -33,23 +30,19 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
-import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
-import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
-import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
+import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
-import java.io.ByteArrayInputStream;
-import java.io.DataInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.MessageDigest;
@@ -117,18 +110,34 @@ public class SignalServiceAccountManager {
return this.pushServiceSocket.getSenderCertificateLegacy();
}
- public void setPin(Optional pin) throws IOException {
- if (pin.isPresent()) {
- this.pushServiceSocket.setPin(pin.get());
- } else {
- this.pushServiceSocket.removePin();
- }
+ /**
+ * @deprecated Remove this method once KBS is live.
+ */
+ @Deprecated
+ public void setPin(String pin) throws IOException {
+ this.pushServiceSocket.setPin(pin);
+ }
+
+ /**
+ * V1 Pin setting has been replaced by KeyBackupService.
+ * Now you can only remove the old pin but there is no need to remove the old pin if setting a KBS Pin.
+ */
+ public void removeV1Pin() throws IOException {
+ this.pushServiceSocket.removePin();
}
public UUID getOwnUuid() throws IOException {
return this.pushServiceSocket.getOwnUuid();
}
+ public KeyBackupService getKeyBackupService(KeyStore iasKeyStore,
+ String enclaveName,
+ String mrenclave,
+ int tries)
+ {
+ return new KeyBackupService(iasKeyStore, enclaveName, mrenclave, pushServiceSocket, tries);
+ }
+
/**
* Register/Unregister a Google Cloud Messaging registration ID.
*
@@ -193,16 +202,20 @@ public class SignalServiceAccountManager {
* This value should remain consistent across registrations for the
* same install, but probabilistically differ across registrations
* for separate installs.
+ * @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS.
+ * @param registrationLock Only supply if found on KBS.
* @return The UUID of the user that was registered.
* @throws IOException
*/
- public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
- byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
+ public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
+ String pin, String registrationLock,
+ byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
signalProtocolRegistrationId,
- fetchesMessages, pin,
+ fetchesMessages,
+ pin, registrationLock,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess);
}
@@ -215,14 +228,18 @@ public class SignalServiceAccountManager {
* This value should remain consistent across registrations for the same
* install, but probabilistically differ across registrations for
* separate installs.
+ * @param pin Only supply if pin has not yet been migrated to KBS.
+ * @param registrationLock Only supply if found on KBS.
*
* @throws IOException
*/
- public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
+ public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
+ String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
- this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages, pin,
+ this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages,
+ pin, registrationLock,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
}
@@ -306,35 +323,21 @@ public class SignalServiceAccountManager {
return activeTokens;
}
- public List getRegisteredUsers(KeyStore iasKeyStore, Set e164numbers, String mrenclave)
+ public List getRegisteredUsers(KeyStore iasKeyStore, Set e164numbers, String enclaveId)
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
{
try {
- String authorization = this.pushServiceSocket.getContactDiscoveryAuthorization();
- Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
- Curve25519KeyPair keyPair = curve.generateKeyPair();
-
- ContactDiscoveryCipher cipher = new ContactDiscoveryCipher();
- RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
- Pair> attestationResponse = this.pushServiceSocket.getContactDiscoveryRemoteAttestation(authorization, attestationRequest, mrenclave);
-
- RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.first().getServerEphemeralPublic(), attestationResponse.first().getServerStaticPublic());
- Quote quote = new Quote(attestationResponse.first().getQuote());
- byte[] requestId = cipher.getRequestId(keys, attestationResponse.first());
-
- cipher.verifyServerQuote(quote, attestationResponse.first().getServerStaticPublic(), mrenclave);
- cipher.verifyIasSignature(iasKeyStore, attestationResponse.first().getCertificates(), attestationResponse.first().getSignatureBody(), attestationResponse.first().getSignature(), quote);
-
- RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys);
- List addressBook = new LinkedList<>();
+ String authorization = pushServiceSocket.getContactDiscoveryAuthorization();
+ RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization);
+ List addressBook = new LinkedList<>();
for (String e164number : e164numbers) {
addressBook.add(e164number.substring(1));
}
- DiscoveryRequest request = cipher.createDiscoveryRequest(addressBook, remoteAttestation);
- DiscoveryResponse response = this.pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, attestationResponse.second(), mrenclave);
- byte[] data = cipher.getDiscoveryResponseData(response, remoteAttestation);
+ DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, remoteAttestation);
+ DiscoveryResponse response = pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, remoteAttestation.getCookies(), enclaveId);
+ byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, remoteAttestation);
Iterator addressBookIterator = addressBook.iterator();
List results = new LinkedList<>();
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/TokenException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/TokenException.java
new file mode 100644
index 0000000000..baea0c0cb8
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/TokenException.java
@@ -0,0 +1,22 @@
+package org.whispersystems.signalservice.api;
+
+import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
+
+class TokenException extends Exception {
+
+ private final TokenResponse nextToken;
+ private final boolean canAutomaticallyRetry;
+
+ TokenException(TokenResponse nextToken, boolean canAutomaticallyRetry) {
+ this.nextToken = nextToken;
+ this.canAutomaticallyRetry = canAutomaticallyRetry;
+ }
+
+ public TokenResponse getToken() {
+ return nextToken;
+ }
+
+ public boolean isCanAutomaticallyRetry() {
+ return canAutomaticallyRetry;
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalKeyBackupServiceUrl.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalKeyBackupServiceUrl.java
new file mode 100644
index 0000000000..2dfc6717ad
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalKeyBackupServiceUrl.java
@@ -0,0 +1,17 @@
+package org.whispersystems.signalservice.internal.configuration;
+
+
+import org.whispersystems.signalservice.api.push.TrustStore;
+
+import okhttp3.ConnectionSpec;
+
+public class SignalKeyBackupServiceUrl extends SignalUrl {
+
+ public SignalKeyBackupServiceUrl(String url, TrustStore trustStore) {
+ super(url, trustStore);
+ }
+
+ public SignalKeyBackupServiceUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) {
+ super(url, hostHeader, trustStore, connectionSpec);
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java
index 86806cd0c8..fdd528acac 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java
@@ -6,11 +6,16 @@ public class SignalServiceConfiguration {
private final SignalServiceUrl[] signalServiceUrls;
private final SignalCdnUrl[] signalCdnUrls;
private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls;
+ private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
- public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, SignalCdnUrl[] signalCdnUrls, SignalContactDiscoveryUrl[] signalContactDiscoveryUrls) {
+ public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
+ SignalCdnUrl[] signalCdnUrls,
+ SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
+ SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls) {
this.signalServiceUrls = signalServiceUrls;
this.signalCdnUrls = signalCdnUrls;
this.signalContactDiscoveryUrls = signalContactDiscoveryUrls;
+ this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
}
public SignalServiceUrl[] getSignalServiceUrls() {
@@ -24,4 +29,8 @@ public class SignalServiceConfiguration {
public SignalContactDiscoveryUrl[] getSignalContactDiscoveryUrls() {
return signalContactDiscoveryUrls;
}
+
+ public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() {
+ return signalKeyBackupServiceUrls;
+ }
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/AESCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/AESCipher.java
new file mode 100644
index 0000000000..19b7fdc4a6
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/AESCipher.java
@@ -0,0 +1,69 @@
+package org.whispersystems.signalservice.internal.contacts.crypto;
+
+import org.whispersystems.libsignal.util.ByteUtil;
+import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
+import org.whispersystems.signalservice.internal.util.Util;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+final class AESCipher {
+
+ private static final int TAG_LENGTH_BYTES = 16;
+ private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8;
+
+ static byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException {
+ try {
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, iv));
+
+ return cipher.doFinal(ByteUtil.combine(ciphertext, tag));
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
+ throw new AssertionError(e);
+ } catch (InvalidKeyException | BadPaddingException e) {
+ throw new InvalidCiphertextException(e);
+ }
+ }
+
+ static AESEncryptedResult encrypt(byte[] key, byte[] aad, byte[] requestData) {
+ try {
+ byte[] iv = Util.getSecretBytes(12);
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, iv));
+ cipher.updateAAD(aad);
+
+ byte[] cipherText = cipher.doFinal(requestData);
+ byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);
+
+ byte[] mac = parts[1];
+ byte[] data = parts[0];
+
+ return new AESEncryptedResult(iv, data, mac, aad);
+ } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ static class AESEncryptedResult {
+ final byte[] iv;
+ final byte[] data;
+ final byte[] mac;
+ final byte[] aad;
+
+ private AESEncryptedResult(byte[] iv, byte[] data, byte[] mac, byte[] aad) {
+ this.iv = iv;
+ this.data = data;
+ this.mac = mac;
+ this.aad = aad;
+ }
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/ContactDiscoveryCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/ContactDiscoveryCipher.java
index 1e3e46efc5..d2cf0d2087 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/ContactDiscoveryCipher.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/ContactDiscoveryCipher.java
@@ -1,47 +1,20 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
-
-import org.threeten.bp.Instant;
-import org.threeten.bp.LocalDateTime;
-import org.threeten.bp.Period;
-import org.threeten.bp.ZoneId;
-import org.threeten.bp.ZonedDateTime;
-import org.threeten.bp.format.DateTimeFormatter;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
-import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
-import org.whispersystems.signalservice.internal.util.Hex;
-import org.whispersystems.signalservice.internal.util.JsonUtil;
-import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.KeyStore;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SignatureException;
-import java.security.cert.CertPathValidatorException;
-import java.security.cert.CertificateException;
import java.util.List;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
+public final class ContactDiscoveryCipher {
-public class ContactDiscoveryCipher {
+ private ContactDiscoveryCipher() {
+ }
- private static final int TAG_LENGTH_BYTES = 16;
- private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8;
- private static final long SIGNATURE_BODY_VERSION = 3L;
-
- public DiscoveryRequest createDiscoveryRequest(List addressBook, RemoteAttestation remoteAttestation) {
+ public static DiscoveryRequest createDiscoveryRequest(List addressBook, RemoteAttestation remoteAttestation) {
try {
ByteArrayOutputStream requestDataStream = new ByteArrayOutputStream();
@@ -49,100 +22,19 @@ public class ContactDiscoveryCipher {
requestDataStream.write(ByteUtil.longToByteArray(Long.parseLong(address)));
}
- byte[] requestData = requestDataStream.toByteArray();
- byte[] nonce = Util.getSecretBytes(12);
- Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ byte[] clientKey = remoteAttestation.getKeys().getClientKey();
+ byte[] requestData = requestDataStream.toByteArray();
+ byte[] aad = remoteAttestation.getRequestId();
- cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(remoteAttestation.getKeys().getClientKey(), "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, nonce));
- cipher.updateAAD(remoteAttestation.getRequestId());
+ AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData);
- byte[] cipherText = cipher.doFinal(requestData);
- byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);
-
- return new DiscoveryRequest(addressBook.size(), remoteAttestation.getRequestId(), nonce, parts[0], parts[1]);
- } catch (IOException | NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
- throw new AssertionError(e);
- }
- }
-
- public byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
- return decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
- }
-
- public byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException {
- return decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag());
- }
-
- public void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave)
- throws UnauthenticatedQuoteException
- {
- try {
- byte[] theirServerPublicStatic = new byte[serverPublicStatic.length];
- System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length);
-
- if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) {
- throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!");
- }
-
- if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) {
- throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave()));
- }
-
- if (quote.isDebugQuote()) {
- throw new UnauthenticatedQuoteException("Received quote for debuggable enclave");
- }
+ return new DiscoveryRequest(addressBook.size(), aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac);
} catch (IOException e) {
- throw new UnauthenticatedQuoteException(e);
- }
- }
-
- public void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote)
- throws SignatureException
- {
- if (certificates == null || certificates.isEmpty()) {
- throw new SignatureException("No certificates.");
- }
-
- try {
- SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore);
- signingCertificate.verifySignature(signatureBody, signature);
-
- SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class);
-
- if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) {
- throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion());
- }
-
- if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) {
- throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes()));
- }
-
- if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) {
- throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus());
- }
-
- if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC")))
- .plus(Period.ofDays(1))
- .isBefore(Instant.now()))
- {
- throw new SignatureException("Signature is expired");
- }
-
- } catch (CertificateException | CertPathValidatorException | IOException e) {
- throw new SignatureException(e);
- }
- }
-
- private byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException {
- try {
- Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
- cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
-
- return cipher.doFinal(ByteUtil.combine(ciphertext, tag));
- } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
- } catch (InvalidKeyException | BadPaddingException e) {
- throw new InvalidCiphertextException(e);
}
}
+
+ public static byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
+ return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
+ }
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/KeyBackupCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/KeyBackupCipher.java
new file mode 100644
index 0000000000..b7a16f3ad2
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/KeyBackupCipher.java
@@ -0,0 +1,128 @@
+package org.whispersystems.signalservice.internal.contacts.crypto;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
+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.keybackup.protos.BackupRequest;
+import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse;
+import org.whispersystems.signalservice.internal.keybackup.protos.DeleteRequest;
+import org.whispersystems.signalservice.internal.keybackup.protos.DeleteResponse;
+import org.whispersystems.signalservice.internal.keybackup.protos.Request;
+import org.whispersystems.signalservice.internal.keybackup.protos.Response;
+import org.whispersystems.signalservice.internal.keybackup.protos.RestoreRequest;
+import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
+
+import java.util.concurrent.TimeUnit;
+
+public final class KeyBackupCipher {
+
+ private KeyBackupCipher() {
+ }
+
+ private static final long VALID_FROM_BUFFER_MS = TimeUnit.DAYS.toMillis(1);
+
+ public static KeyBackupRequest createKeyBackupRequest(byte[] kbsAccessKey,
+ byte[] kbsData,
+ TokenResponse token,
+ RemoteAttestation remoteAttestation,
+ byte[] serviceId,
+ int tries)
+ {
+ long now = System.currentTimeMillis();
+
+ BackupRequest backupRequest = BackupRequest.newBuilder()
+ .setServiceId(ByteString.copyFrom(serviceId))
+ .setBackupId(ByteString.copyFrom(token.getBackupId()))
+ .setToken(ByteString.copyFrom(token.getToken()))
+ .setValidFrom(getValidFromSeconds(now))
+ .setData(ByteString.copyFrom(kbsData))
+ .setPin(ByteString.copyFrom(kbsAccessKey))
+ .setTries(tries)
+ .build();
+
+ Request requestData = Request.newBuilder().setBackup(backupRequest).build();
+
+ return createKeyBackupRequest(requestData, remoteAttestation);
+ }
+
+ public static KeyBackupRequest createKeyRestoreRequest(byte[] kbsAccessKey,
+ TokenResponse token,
+ RemoteAttestation remoteAttestation,
+ byte[] serviceId)
+ {
+ long now = System.currentTimeMillis();
+
+ RestoreRequest restoreRequest = RestoreRequest.newBuilder()
+ .setServiceId(ByteString.copyFrom(serviceId))
+ .setBackupId(ByteString.copyFrom(token.getBackupId()))
+ .setToken(ByteString.copyFrom(token.getToken()))
+ .setValidFrom(getValidFromSeconds(now))
+ .setPin(ByteString.copyFrom(kbsAccessKey))
+ .build();
+
+ Request request = Request.newBuilder().setRestore(restoreRequest).build();
+
+ return createKeyBackupRequest(request, remoteAttestation);
+ }
+
+ public static KeyBackupRequest createKeyDeleteRequest(TokenResponse token,
+ RemoteAttestation remoteAttestation,
+ byte[] serviceId)
+ {
+ DeleteRequest deleteRequest = DeleteRequest.newBuilder()
+ .setServiceId(ByteString.copyFrom(serviceId))
+ .setBackupId(ByteString.copyFrom(token.getBackupId()))
+ .build();
+
+ Request request = Request.newBuilder().setDelete(deleteRequest).build();
+
+ return createKeyBackupRequest(request, remoteAttestation);
+ }
+
+ public static BackupResponse getKeyBackupResponse(KeyBackupResponse response, RemoteAttestation remoteAttestation)
+ throws InvalidCiphertextException, InvalidProtocolBufferException
+ {
+ byte[] data = decryptData(response, remoteAttestation);
+
+ Response backupResponse = Response.parseFrom(data);
+
+ return backupResponse.getBackup();
+ }
+
+ public static RestoreResponse getKeyRestoreResponse(KeyBackupResponse response, RemoteAttestation remoteAttestation)
+ throws InvalidCiphertextException, InvalidProtocolBufferException
+ {
+ byte[] data = decryptData(response, remoteAttestation);
+
+ return Response.parseFrom(data).getRestore();
+ }
+
+ public static DeleteResponse getKeyDeleteResponseStatus(KeyBackupResponse response, RemoteAttestation remoteAttestation)
+ throws InvalidCiphertextException, InvalidProtocolBufferException
+ {
+ byte[] data = decryptData(response, remoteAttestation);
+
+ return DeleteResponse.parseFrom(data);
+ }
+
+ private static KeyBackupRequest createKeyBackupRequest(Request requestData, RemoteAttestation remoteAttestation) {
+ byte[] clientKey = remoteAttestation.getKeys().getClientKey();
+ byte[] aad = remoteAttestation.getRequestId();
+
+ AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData.toByteArray());
+
+ return new KeyBackupRequest(aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac);
+ }
+
+ private static byte[] decryptData(KeyBackupResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
+ return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
+ }
+
+ private static long getValidFromSeconds(long nowMs) {
+ return TimeUnit.MILLISECONDS.toSeconds(nowMs - VALID_FROM_BUFFER_MS);
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestation.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestation.java
index 7e0c7e953c..62ad1cfdf1 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestation.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestation.java
@@ -1,13 +1,17 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
+import java.util.List;
+
public class RemoteAttestation {
private final byte[] requestId;
private final RemoteAttestationKeys keys;
+ private final List cookies;
- public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys) {
+ public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys, List cookies) {
this.requestId = requestId;
this.keys = keys;
+ this.cookies = cookies;
}
public byte[] getRequestId() {
@@ -17,4 +21,8 @@ public class RemoteAttestation {
public RemoteAttestationKeys getKeys() {
return keys;
}
+
+ public List getCookies() {
+ return cookies;
+ }
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java
new file mode 100644
index 0000000000..7bd3481c71
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java
@@ -0,0 +1,92 @@
+package org.whispersystems.signalservice.internal.contacts.crypto;
+
+import org.threeten.bp.Instant;
+import org.threeten.bp.LocalDateTime;
+import org.threeten.bp.Period;
+import org.threeten.bp.ZoneId;
+import org.threeten.bp.ZonedDateTime;
+import org.threeten.bp.format.DateTimeFormatter;
+import org.whispersystems.libsignal.util.ByteUtil;
+import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
+import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
+import org.whispersystems.signalservice.internal.util.Hex;
+import org.whispersystems.signalservice.internal.util.JsonUtil;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.MessageDigest;
+import java.security.SignatureException;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertificateException;
+
+public final class RemoteAttestationCipher {
+
+ private RemoteAttestationCipher() {
+ }
+
+ private static final long SIGNATURE_BODY_VERSION = 3L;
+
+ public static byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException {
+ return AESCipher.decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag());
+ }
+
+ public static void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave)
+ throws UnauthenticatedQuoteException
+ {
+ try {
+ byte[] theirServerPublicStatic = new byte[serverPublicStatic.length];
+ System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length);
+
+ if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) {
+ throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!");
+ }
+
+ if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) {
+ throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave()));
+ }
+
+ if (quote.isDebugQuote()) {
+ throw new UnauthenticatedQuoteException("Received quote for debuggable enclave");
+ }
+ } catch (IOException e) {
+ throw new UnauthenticatedQuoteException(e);
+ }
+ }
+
+ public static void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote)
+ throws SignatureException
+ {
+ if (certificates == null || certificates.isEmpty()) {
+ throw new SignatureException("No certificates.");
+ }
+
+ try {
+ SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore);
+ signingCertificate.verifySignature(signatureBody, signature);
+
+ SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class);
+
+ if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) {
+ throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion());
+ }
+
+ if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) {
+ throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes()));
+ }
+
+ if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) {
+ throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus());
+ }
+
+ if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC")))
+ .plus(Period.ofDays(1))
+ .isBefore(Instant.now()))
+ {
+ throw new SignatureException("Signature is expired");
+ }
+
+ } catch (CertificateException | CertPathValidatorException | IOException e) {
+ throw new SignatureException(e);
+ }
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/SigningCertificate.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/SigningCertificate.java
index f26fd7605e..ad90b40cc4 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/SigningCertificate.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/SigningCertificate.java
@@ -31,11 +31,15 @@ public class SigningCertificate {
{
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
- Collection certificatesCollection = (Collection) certificateFactory.generateCertificates(new ByteArrayInputStream(URLDecoder.decode(certificateChain).getBytes()));
+ Collection certificatesCollection = (Collection) certificateFactory.generateCertificates(new ByteArrayInputStream(certificateChain.getBytes()));
List certificates = new LinkedList<>(certificatesCollection);
PKIXParameters pkixParameters = new PKIXParameters(trustStore);
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
+ if (certificates.isEmpty()) {
+ throw new CertificateException("No certificates available! Badly-formatted cert chain?");
+ }
+
this.path = certificateFactory.generateCertPath(certificates);
pkixParameters.setRevocationEnabled(false);
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/DiscoveryRequest.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/DiscoveryRequest.java
index e10cb9be25..3ed0259ed9 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/DiscoveryRequest.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/DiscoveryRequest.java
@@ -47,7 +47,7 @@ public class DiscoveryRequest {
this.requestId = requestId;
this.iv = iv;
this.data = data;
- this. mac = mac;
+ this.mac = mac;
}
public byte[] getRequestId() {
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupRequest.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupRequest.java
new file mode 100644
index 0000000000..4bc5c7cde1
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupRequest.java
@@ -0,0 +1,51 @@
+package org.whispersystems.signalservice.internal.contacts.entities;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.whispersystems.signalservice.internal.util.Hex;
+
+public class KeyBackupRequest {
+
+ @JsonProperty
+ private byte[] requestId;
+
+ @JsonProperty
+ private byte[] iv;
+
+ @JsonProperty
+ private byte[] data;
+
+ @JsonProperty
+ private byte[] mac;
+
+ public KeyBackupRequest() {
+ }
+
+ public KeyBackupRequest(byte[] requestId, byte[] iv, byte[] data, byte[] mac) {
+ this.requestId = requestId;
+ this.iv = iv;
+ this.data = data;
+ this.mac = mac;
+ }
+
+ public byte[] getRequestId() {
+ return requestId;
+ }
+
+ public byte[] getIv() {
+ return iv;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+
+ public byte[] getMac() {
+ return mac;
+ }
+
+ public String toString() {
+ return "{ requestId: " + Hex.toString(requestId) + ", iv: " + Hex.toString(iv) + ", data: " + Hex.toString(data) + ", mac: " + Hex.toString(mac) + "}";
+ }
+
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupResponse.java
new file mode 100644
index 0000000000..76bb663f33
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupResponse.java
@@ -0,0 +1,41 @@
+package org.whispersystems.signalservice.internal.contacts.entities;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.whispersystems.signalservice.internal.util.Hex;
+
+public class KeyBackupResponse {
+
+ @JsonProperty
+ private byte[] iv;
+
+ @JsonProperty
+ private byte[] data;
+
+ @JsonProperty
+ private byte[] mac;
+
+ public KeyBackupResponse() {}
+
+ public KeyBackupResponse(byte[] iv, byte[] data, byte[] mac) {
+ this.iv = iv;
+ this.data = data;
+ this.mac = mac;
+ }
+
+ public byte[] getIv() {
+ return iv;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+
+ public byte[] getMac() {
+ return mac;
+ }
+
+ public String toString() {
+ return "{iv: " + (iv == null ? null : Hex.toString(iv)) + ", data: " + (data == null ? null: Hex.toString(data)) + ", mac: " + (mac == null ? null : Hex.toString(mac)) + "}";
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/TokenResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/TokenResponse.java
new file mode 100644
index 0000000000..483e705c52
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/TokenResponse.java
@@ -0,0 +1,38 @@
+package org.whispersystems.signalservice.internal.contacts.entities;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class TokenResponse {
+
+ @JsonProperty
+ private byte[] backupId;
+
+ @JsonProperty
+ private byte[] token;
+
+ @JsonProperty
+ private int tries;
+
+ @JsonCreator
+ public TokenResponse() {
+ }
+
+ public TokenResponse(byte[] backupId, byte[] token, int tries) {
+ this.backupId = backupId;
+ this.token = token;
+ this.tries = tries;
+ }
+
+ public byte[] getBackupId() {
+ return backupId;
+ }
+
+ public byte[] getToken() {
+ return token;
+ }
+
+ public int getTries() {
+ return tries;
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java
index c46141c99f..ad881637d3 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java
@@ -28,19 +28,23 @@ public class AccountAttributes {
@JsonProperty
private String pin;
+ @JsonProperty
+ private String registrationLock;
+
@JsonProperty
private byte[] unidentifiedAccessKey;
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
- public AccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) {
+ public AccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) {
this.signalingKey = signalingKey;
this.registrationId = registrationId;
this.voice = true;
this.video = true;
this.fetchesMessages = fetchesMessages;
this.pin = pin;
+ this.registrationLock = registrationLock;
this.unidentifiedAccessKey = unidentifiedAccessKey;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
}
@@ -71,6 +75,10 @@ public class AccountAttributes {
return pin;
}
+ public String getRegistrationLock() {
+ return registrationLock;
+ }
+
public byte[] getUnidentifiedAccessKey() {
return unidentifiedAccessKey;
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AuthCredentials.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AuthCredentials.java
new file mode 100644
index 0000000000..a6dec30bb6
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AuthCredentials.java
@@ -0,0 +1,18 @@
+package org.whispersystems.signalservice.internal.push;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import okhttp3.Credentials;
+
+public class AuthCredentials {
+
+ @JsonProperty
+ private String username;
+
+ @JsonProperty
+ private String password;
+
+ public String asBasic() {
+ return Credentials.basic(username, password);
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ContactDiscoveryCredentials.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ContactDiscoveryCredentials.java
deleted file mode 100644
index 35e0d99022..0000000000
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ContactDiscoveryCredentials.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.whispersystems.signalservice.internal.push;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class ContactDiscoveryCredentials {
-
- @JsonProperty
- private String username;
-
- @JsonProperty
- private String password;
-
- public void setUsername(String username) {
- this.username = username;
- }
-
- public void setPassword(String password) {
- this.password = password;
- }
-
- public String getUsername() {
- return username;
- }
-
- public String getPassword() {
- return password;
- }
-}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java
index bc48509429..37ad5701cc 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java
@@ -3,14 +3,16 @@ package org.whispersystems.signalservice.internal.push;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
-public class LockedException extends NonSuccessfulResponseCodeException {
+public final class LockedException extends NonSuccessfulResponseCodeException {
- private int length;
- private long timeRemaining;
+ private final int length;
+ private final long timeRemaining;
+ private final String basicStorageCredentials;
- LockedException(int length, long timeRemaining) {
- this.length = length;
- this.timeRemaining = timeRemaining;
+ LockedException(int length, long timeRemaining, String basicStorageCredentials) {
+ this.length = length;
+ this.timeRemaining = timeRemaining;
+ this.basicStorageCredentials = basicStorageCredentials;
}
public int getLength() {
@@ -20,4 +22,8 @@ public class LockedException extends NonSuccessfulResponseCodeException {
public long getTimeRemaining() {
return timeRemaining;
}
+
+ public String getBasicStorageCredentials() {
+ return basicStorageCredentials;
+ }
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
index f051c4e2a0..9bf00a11ee 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
@@ -43,8 +43,9 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.configuration.SignalUrl;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
-import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
-import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
+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.push.exceptions.MismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
@@ -78,14 +79,15 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.Call;
import okhttp3.ConnectionSpec;
-import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
@@ -108,6 +110,7 @@ public class PushServiceSocket {
private static final String TURN_SERVER_INFO = "/v1/accounts/turn";
private static final String SET_ACCOUNT_ATTRIBUTES = "/v1/accounts/attributes/";
private static final String PIN_PATH = "/v1/accounts/pin/";
+ private static final String REGISTRATION_LOCK_PATH = "/v1/accounts/registration_lock";
private static final String REQUEST_PUSH_CHALLENGE = "/v1/accounts/fcm/preauth/%s/%s";
private static final String WHO_AM_I = "/v1/accounts/whoami";
private static final String SET_USERNAME_PATH = "/v1/accounts/username/%s";
@@ -137,6 +140,8 @@ public class PushServiceSocket {
private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery";
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true";
+ private static final String KBS_AUTH_PATH = "/v1/backup/auth";
+
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
@@ -152,6 +157,7 @@ public class PushServiceSocket {
private final ServiceConnectionHolder[] serviceClients;
private final ConnectionHolder[] cdnClients;
private final ConnectionHolder[] contactDiscoveryClients;
+ private final ConnectionHolder[] keyBackupServiceClients;
private final OkHttpClient attachmentClient;
private final CredentialsProvider credentialsProvider;
@@ -164,6 +170,7 @@ public class PushServiceSocket {
this.serviceClients = createServiceConnectionHolders(signalServiceConfiguration.getSignalServiceUrls());
this.cdnClients = createConnectionHolders(signalServiceConfiguration.getSignalCdnUrls());
this.contactDiscoveryClients = createConnectionHolders(signalServiceConfiguration.getSignalContactDiscoveryUrls());
+ this.keyBackupServiceClients = createConnectionHolders(signalServiceConfiguration.getSignalKeyBackupServiceUrls());
this.attachmentClient = createAttachmentClient();
this.random = new SecureRandom();
}
@@ -219,11 +226,12 @@ public class PushServiceSocket {
}
}
- public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, String pin,
+ public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages,
+ String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
- AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
+ AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
String requestBody = JsonUtil.toJson(signalingKeyEntity);
String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody);
VerifyAccountResponse response = JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
@@ -236,11 +244,16 @@ public class PushServiceSocket {
}
}
- public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin,
+ public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages,
+ String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
- AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin,
+ if (registrationLock != null && pin != null) {
+ throw new AssertionError("Pin should be null if registrationLock is set.");
+ }
+
+ AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes));
}
@@ -282,10 +295,20 @@ public class PushServiceSocket {
makeServiceRequest(PIN_PATH, "PUT", JsonUtil.toJson(accountLock));
}
+ /** Note: Setting a KBS Pin will clear this */
public void removePin() throws IOException {
makeServiceRequest(PIN_PATH, "DELETE", null);
}
+ public void setRegistrationLock(String registrationLock) throws IOException {
+ RegistrationLockV2 accountLock = new RegistrationLockV2(registrationLock);
+ makeServiceRequest(REGISTRATION_LOCK_PATH, "PUT", JsonUtil.toJson(accountLock));
+ }
+
+ public void removePinV2() throws IOException {
+ makeServiceRequest(REGISTRATION_LOCK_PATH, "DELETE", null);
+ }
+
public byte[] getSenderCertificateLegacy() throws IOException {
String responseText = makeServiceRequest(SENDER_CERTIFICATE_LEGACY_PATH, "GET", null);
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
@@ -592,26 +615,27 @@ public class PushServiceSocket {
}
}
- public String getContactDiscoveryAuthorization() throws IOException {
- String response = makeServiceRequest(DIRECTORY_AUTH_PATH, "GET", null);
- ContactDiscoveryCredentials token = JsonUtil.fromJson(response, ContactDiscoveryCredentials.class);
- return Credentials.basic(token.getUsername(), token.getPassword());
+ private String getCredentials(String authPath) throws IOException {
+ String response = makeServiceRequest(authPath, "GET", null, NO_HEADERS);
+ AuthCredentials token = JsonUtil.fromJson(response, AuthCredentials.class);
+ return token.asBasic();
}
- public Pair> getContactDiscoveryRemoteAttestation(String authorization, RemoteAttestationRequest request, String mrenclave)
- throws IOException
- {
- Response response = makeContactDiscoveryRequest(authorization, new LinkedList(), "/v1/attestation/" + mrenclave, "PUT", JsonUtil.toJson(request));
- ResponseBody body = response.body();
- List rawCookies = response.headers("Set-Cookie");
- List cookies = new LinkedList<>();
+ public String getContactDiscoveryAuthorization() throws IOException {
+ return getCredentials(DIRECTORY_AUTH_PATH);
+ }
- for (String cookie : rawCookies) {
- cookies.add(cookie.split(";")[0]);
- }
+ public String getKeyBackupServiceAuthorization() throws IOException {
+ return getCredentials(KBS_AUTH_PATH);
+ }
+
+ public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName)
+ throws IOException
+ {
+ ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, null, "/v1/token/" + enclaveName, "GET", null).body();
if (body != null) {
- return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
+ return JsonUtil.fromJson(body.string(), TokenResponse.class);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
@@ -620,7 +644,7 @@ public class PushServiceSocket {
public DiscoveryResponse getContactDiscoveryRegisteredUsers(String authorizationToken, DiscoveryRequest request, List cookies, String mrenclave)
throws IOException
{
- ResponseBody body = makeContactDiscoveryRequest(authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
+ ResponseBody body = makeRequest(ClientSet.ContactDiscovery, authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
if (body != null) {
return JsonUtil.fromJson(body.string(), DiscoveryResponse.class);
@@ -629,6 +653,18 @@ public class PushServiceSocket {
}
}
+ public KeyBackupResponse putKbsData(String authorizationToken, KeyBackupRequest request, List cookies, String mrenclave)
+ throws IOException
+ {
+ ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, cookies, "/v1/backup/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
+
+ if (body != null) {
+ return JsonUtil.fromJson(body.string(), KeyBackupResponse.class);
+ } else {
+ throw new NonSuccessfulResponseCodeException("Empty response!");
+ }
+ }
+
public void reportContactDiscoveryServiceMatch() throws IOException {
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "ok"), "PUT", "");
}
@@ -922,7 +958,12 @@ public class PushServiceSocket {
throw new PushNetworkException(e);
}
- throw new LockedException(accountLockFailure.length, accountLockFailure.timeRemaining);
+ AuthCredentials credentials = accountLockFailure.backupCredentials;
+ String basicStorageCredentials = credentials != null ? credentials.asBasic() : null;
+
+ throw new LockedException(accountLockFailure.length,
+ accountLockFailure.timeRemaining,
+ basicStorageCredentials);
}
if (responseCode != 200 && responseCode != 204) {
@@ -960,10 +1001,12 @@ public class PushServiceSocket {
request.addHeader(header.getKey(), header.getValue());
}
- if (unidentifiedAccess.isPresent()) {
- request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
- } else if (credentialsProvider.getPassword() != null) {
- request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
+ if (!headers.containsKey("Authorization")) {
+ if (unidentifiedAccess.isPresent()) {
+ request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
+ } else if (credentialsProvider.getPassword() != null) {
+ request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
+ }
}
if (userAgent != null) {
@@ -992,15 +1035,33 @@ public class PushServiceSocket {
}
}
- private Response makeContactDiscoveryRequest(String authorization, List cookies, String path, String method, String body)
+ private ConnectionHolder[] clientsFor(ClientSet clientSet) {
+ switch (clientSet) {
+ case ContactDiscovery:
+ return contactDiscoveryClients;
+ case KeyBackup:
+ return keyBackupServiceClients;
+ default:
+ throw new AssertionError("Unknown attestation purpose");
+ }
+ }
+
+ Response makeRequest(ClientSet clientSet, String authorization, List cookies, String path, String method, String body)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
- ConnectionHolder connectionHolder = getRandom(contactDiscoveryClients, random);
- OkHttpClient okHttpClient = connectionHolder.getClient()
- .newBuilder()
- .connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
- .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
- .build();
+ ConnectionHolder connectionHolder = getRandom(clientsFor(clientSet), random);
+
+ return makeRequest(connectionHolder, authorization, cookies, path, method, body);
+ }
+
+ private Response makeRequest(ConnectionHolder connectionHolder, String authorization, List cookies, String path, String method, String body)
+ throws PushNetworkException, NonSuccessfulResponseCodeException
+ {
+ OkHttpClient okHttpClient = connectionHolder.getClient()
+ .newBuilder()
+ .connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
+ .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
+ .build();
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
@@ -1153,12 +1214,26 @@ public class PushServiceSocket {
}
}
+ private static class RegistrationLockV2 {
+ @JsonProperty
+ private String registrationLock;
+
+ public RegistrationLockV2() {}
+
+ public RegistrationLockV2(String registrationLock) {
+ this.registrationLock = registrationLock;
+ }
+ }
+
private static class RegistrationLockFailure {
@JsonProperty
private int length;
@JsonProperty
private long timeRemaining;
+
+ @JsonProperty
+ private AuthCredentials backupCredentials;
}
private static class AttachmentDescriptor {
@@ -1225,4 +1300,6 @@ public class PushServiceSocket {
@Override
public void handle(int responseCode) { }
}
+
+ public enum ClientSet { ContactDiscovery, KeyBackup }
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteAttestationUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteAttestationUtil.java
new file mode 100644
index 0000000000..4cc4cd30f4
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteAttestationUtil.java
@@ -0,0 +1,79 @@
+package org.whispersystems.signalservice.internal.push;
+
+import org.whispersystems.curve25519.Curve25519;
+import org.whispersystems.curve25519.Curve25519KeyPair;
+import org.whispersystems.libsignal.util.Pair;
+import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
+import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
+import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
+import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
+import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationCipher;
+import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
+import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
+import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
+import org.whispersystems.signalservice.internal.util.JsonUtil;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.SignatureException;
+import java.util.LinkedList;
+import java.util.List;
+
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+public final class RemoteAttestationUtil {
+
+ private RemoteAttestationUtil() {
+ }
+
+ public static RemoteAttestation getAndVerifyRemoteAttestation(PushServiceSocket socket,
+ PushServiceSocket.ClientSet clientSet,
+ KeyStore iasKeyStore,
+ String enclaveName,
+ String mrenclave,
+ String authorization)
+ throws IOException, Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException
+ {
+ Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
+ Curve25519KeyPair keyPair = curve.generateKeyPair();
+ RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
+ Pair> attestationResponsePair = getRemoteAttestation(socket, clientSet, authorization, attestationRequest, enclaveName);
+ RemoteAttestationResponse attestationResponse = attestationResponsePair.first();
+ List attestationCookies = attestationResponsePair.second();
+
+ RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.getServerEphemeralPublic(), attestationResponse.getServerStaticPublic());
+ Quote quote = new Quote(attestationResponse.getQuote());
+ byte[] requestId = RemoteAttestationCipher.getRequestId(keys, attestationResponse);
+
+ RemoteAttestationCipher.verifyServerQuote(quote, attestationResponse.getServerStaticPublic(), mrenclave);
+
+ RemoteAttestationCipher.verifyIasSignature(iasKeyStore, attestationResponse.getCertificates(), attestationResponse.getSignatureBody(), attestationResponse.getSignature(), quote);
+
+ return new RemoteAttestation(requestId, keys, attestationCookies);
+ }
+
+ private static Pair> getRemoteAttestation(PushServiceSocket socket,
+ PushServiceSocket.ClientSet clientSet,
+ String authorization,
+ RemoteAttestationRequest request,
+ String enclaveName)
+ throws IOException
+ {
+ Response response = socket.makeRequest(clientSet, authorization, new LinkedList(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(request));
+ ResponseBody body = response.body();
+ List rawCookies = response.headers("Set-Cookie");
+ List cookies = new LinkedList<>();
+
+ for (String cookie : rawCookies) {
+ cookies.add(cookie.split(";")[0]);
+ }
+
+ if (body != null) {
+ return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
+ } else {
+ throw new NonSuccessfulResponseCodeException("Empty response!");
+ }
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java
new file mode 100644
index 0000000000..b070fe1e74
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java
@@ -0,0 +1,8 @@
+package org.whispersystems.signalservice.internal.registrationpin;
+
+public final class InvalidPinException extends Exception {
+
+ InvalidPinException(String message) {
+ super(message);
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java
new file mode 100644
index 0000000000..4384d1db4b
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java
@@ -0,0 +1,166 @@
+package org.whispersystems.signalservice.internal.registrationpin;
+
+import org.whispersystems.signalservice.internal.util.Hex;
+import org.whispersystems.signalservice.internal.util.Util;
+
+import java.nio.charset.Charset;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public final class PinStretcher {
+
+ private static final String HMAC_SHA256 = "HmacSHA256";
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ public static StretchedPin stretchPin(CharSequence pin) throws InvalidPinException {
+ return new StretchedPin(pin);
+ }
+
+ public static class StretchedPin {
+ private final byte[] stretchedPin;
+ private final byte[] pinKey1;
+ private final byte[] kbsAccessKey;
+
+ private StretchedPin(byte[] stretchedPin, byte[] pinKey1, byte[] kbsAccessKey) {
+ this.stretchedPin = stretchedPin;
+ this.pinKey1 = pinKey1;
+ this.kbsAccessKey = kbsAccessKey;
+ }
+
+ private StretchedPin(CharSequence pin) throws InvalidPinException {
+ if (pin.length() < 4) throw new InvalidPinException("Pin too short");
+
+ char[] arabicPin = toArabic(pin);
+
+ stretchedPin = pbkdf2HmacSHA256(arabicPin, "nosalt", 20000, 256);
+
+ try {
+ Mac mac = Mac.getInstance(HMAC_SHA256);
+ mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256));
+ mac.update("Master Key Encryption".getBytes(UTF_8));
+
+ pinKey1 = new byte[32];
+ mac.doFinal(pinKey1, 0);
+
+ mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256));
+ mac.update("KBS Access Key".getBytes(UTF_8));
+
+ kbsAccessKey = new byte[32];
+ mac.doFinal(kbsAccessKey, 0);
+ } catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public MasterKey withPinKey2(byte[] pinKey2) {
+ return new MasterKey(pinKey1, pinKey2, this);
+ }
+
+ public MasterKey withNewSecurePinKey2() {
+ return withPinKey2(Util.getSecretBytes(32));
+ }
+
+ public byte[] getPinKey1() {
+ return pinKey1;
+ }
+
+ public byte[] getStretchedPin() {
+ return stretchedPin;
+ }
+
+ public byte[] getKbsAccessKey() {
+ return kbsAccessKey;
+ }
+ }
+
+ public static class MasterKey extends StretchedPin {
+ private final byte[] pinKey2;
+ private final byte[] masterKey;
+ private final String registrationLock;
+
+ private MasterKey(byte[] pinKey1, byte[] pinKey2, StretchedPin stretchedPin) {
+ super(stretchedPin.stretchedPin, stretchedPin.pinKey1, stretchedPin.kbsAccessKey);
+
+ if (pinKey2.length != 32) {
+ throw new AssertionError("PinKey2 must be exactly 32 bytes");
+ }
+
+ this.pinKey2 = pinKey2.clone();
+
+ try {
+ Mac mac = Mac.getInstance(HMAC_SHA256);
+
+ mac.init(new SecretKeySpec(pinKey1, HMAC_SHA256));
+ mac.update(pinKey2);
+
+ masterKey = new byte[32];
+ mac.doFinal(masterKey, 0);
+
+ mac.init(new SecretKeySpec(masterKey, HMAC_SHA256));
+ mac.update("Registration Lock".getBytes(UTF_8));
+
+ byte[] registration_lock_token_bytes = new byte[32];
+ mac.doFinal(registration_lock_token_bytes, 0);
+ registrationLock = Hex.toStringCondensed(registration_lock_token_bytes);
+
+ } catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public byte[] getPinKey2() {
+ return pinKey2;
+ }
+
+ public String getRegistrationLock() {
+ return registrationLock;
+ }
+
+ public byte[] getMasterKey() {
+ return masterKey;
+ }
+ }
+
+ private static byte[] pbkdf2HmacSHA256(char[] pin, String salt, int iterationCount, int outputSize) {
+ byte[] saltBytes = salt.getBytes(Charset.forName("UTF-8"));
+
+ try {
+ SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+ PBEKeySpec spec = new PBEKeySpec(pin, saltBytes, iterationCount, outputSize);
+ byte[] encoded = skf.generateSecret(spec).getEncoded();
+
+ spec.clearPassword();
+
+ return encoded;
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new AssertionError("Could not stretch pin", e);
+ }
+ }
+
+ /**
+ * Converts a string of not necessarily Arabic numerals to Arabic 0..9 characters.
+ */
+ private static char[] toArabic(CharSequence numerals) throws InvalidPinException {
+ int length = numerals.length();
+ char[] arabic = new char[length];
+
+ for (int i = 0; i < length; i++) {
+ int digit = Character.digit(numerals.charAt(i), 10);
+
+ if (digit < 0) {
+ throw new InvalidPinException("Pin must only consist of decimals");
+ }
+
+ arabic[i] = (char) ('0' + digit);
+ }
+
+ return arabic;
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/Util.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/Util.java
index 3de6a2442b..b1a3f5b4af 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/Util.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/Util.java
@@ -10,7 +10,6 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
@@ -71,13 +70,9 @@ public class Util {
}
public static byte[] getSecretBytes(int size) {
- try {
- byte[] secret = new byte[size];
- SecureRandom.getInstance("SHA1PRNG").nextBytes(secret);
- return secret;
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e);
- }
+ byte[] secret = new byte[size];
+ new SecureRandom().nextBytes(secret);
+ return secret;
}
public static byte[] getRandomLengthBytes(int maxSize) {
diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/SigningCertificateTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/SigningCertificateTest.java
index 9c2d69c63f..d5efbcebbd 100644
--- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/SigningCertificateTest.java
+++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/SigningCertificateTest.java
@@ -7,6 +7,7 @@ import org.whispersystems.signalservice.internal.contacts.crypto.SigningCertific
import org.whispersystems.util.Base64;
import java.io.IOException;
+import java.net.URLDecoder;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -22,7 +23,7 @@ public class SigningCertificateTest extends TestCase {
}
public void testGoodSignature() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, CertPathValidatorException, SignatureException {
- String certificateChain = "-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A";
+ String certificateChain = URLDecoder.decode("-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A");
String signature = "Kn2Ya2T039qvEWIzIQeSksNyyCQIkcVjciClcp3a6C766dJANXxLLIn6CfyvUZddMtePrTOLpC2e5QTQxB4RwtWmFfr7nxRdFUtA3dH2DAQL5DqqlmPv46ZWSPfiiOXUsu8vNgX3Z4Znt4Q+dIPIquNPY8ZmiAcpKR7n2K3QtabgOnJ2EyngabY3LMQTtriXbZjpl53ynhVhV1rciMdvMaTz4DUYt7gKi+KeNd3CBFSev+eTgYPC3em96J/3bfVR+wC5m3JGbIBCrwAsbO05JkiNIMck3s+p4d/hwiABR75EplxaWmGgIm6VvUKtGhdJ/cNrmF0nxMX6Vi6N2WaLTA==";
String signatureBody = "{\"id\":\"287419896494669543891634765983074535548\",\"timestamp\":\"2019-03-11T20:01:21.658293\",\"version\":3,\"isvEnclaveQuoteStatus\":\"OK\",\"isvEnclaveQuoteBody\":\"AgAAADILAAAIAAcAAAAAAPiLWcRSSA3shraxepsGV9qF4zYUPJgE42ZZZXS2G9zaBQUCBP//AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAHAAAAAAAAAM1s/DQpN7I7G907v5chqlYVrJ/1CnXFUn1EHNMnaCbJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrzm117Qj8NlEllyDkV4Pae4UgsPjgVXtAA5UsG90gVgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHgz6GaO6bkxfPLBYcR5rEf9Itrt81OEanXteSMcd/BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}";
@@ -35,7 +36,7 @@ public class SigningCertificateTest extends TestCase {
}
public void testBadSignature() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, CertPathValidatorException, SignatureException {
- String certificateChain = "-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A";
+ String certificateChain = URLDecoder.decode("-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A");
String signature = "Kn2Ya2T039qvEWIzIQeSksNyyCQIkcVjciClcp3a6C766dJANXxLLIn6CfyvUZddMtePrTOLpC2e5QTQxB4RwtWmFfr7nxRdFUtA3dH2DAQL5DqqlmPv46ZWSPfiiOXUsu8vNgX3Z4Znt4Q+dIPIquNPY8ZmiAcpKR7n2K3QtabgOnJ2EyngabY3LMQTtriXbZjpl53ynhVhV1rciMdvMaTz4DUYt7gKi+KeNd3CBFSev+eTgYPC3em96J/3bfVR+wC5m3JGbIBCrwAsbO05JkiNIMck3s+p4d/hwiABR75EplxaWmGgIm6VvUKtGhdJ/cNrmF0nxMX6Vi6N2WaLTA==";
String signatureBody = "{\"id\":\"287419896494669543891634765983074535548\",\"timestamp\":\"2019-03-11T20:01:21.658293\",\"version\":3,\"isvEnclaveQuoteStatus\":\"OK\",\"isvEnclaveQuoteBody\":\"AgAAADILAAAIAAcAAAAAAPiLWcRSSA3shraxepsGV9qF4zYUPJgE42ZZZXS2G9zaBQUCBP//AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAHAAAAAAAAAM1s/DQpN7I7G907v5chqlYVrJ/1CnXFUn1EHNMnaCbJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrzm117Qj8NlEllyDkV4Pae4UgsPjgVXtAA5UsG90gVgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHgz6GaO6bkxfPLBYcR5rEf9Itrt81OEanXteSMcd/BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}";
@@ -63,7 +64,7 @@ public class SigningCertificateTest extends TestCase {
}
public void testBadChain() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, CertPathValidatorException, SignatureException {
- String certificateChain = "-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgAA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A";
+ String certificateChain = URLDecoder.decode("-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgAA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A");
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(getClass().getResourceAsStream("/ias.jks"), "whisper".toCharArray());
diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java
new file mode 100644
index 0000000000..d0d7bae50b
--- /dev/null
+++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java
@@ -0,0 +1,31 @@
+package org.whispersystems.signalservice.internal.registrationpin;
+
+import org.junit.Test;
+
+public final class PinStretchFailureTest {
+
+ @Test(expected = InvalidPinException.class)
+ public void non_numeric_pin() throws InvalidPinException {
+ PinStretcher.stretchPin("A");
+ }
+
+ @Test(expected = InvalidPinException.class)
+ public void empty() throws InvalidPinException {
+ PinStretcher.stretchPin("");
+ }
+
+ @Test(expected = InvalidPinException.class)
+ public void too_few_digits() throws InvalidPinException {
+ PinStretcher.stretchPin("123");
+ }
+
+ @Test(expected = AssertionError.class)
+ public void pin_key_2_too_short() throws InvalidPinException {
+ PinStretcher.stretchPin("0000").withPinKey2(new byte[31]);
+ }
+
+ @Test(expected = AssertionError.class)
+ public void pin_key_2_too_long() throws InvalidPinException {
+ PinStretcher.stretchPin("0000").withPinKey2(new byte[33]);
+ }
+}
diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java
new file mode 100644
index 0000000000..4624e1766e
--- /dev/null
+++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java
@@ -0,0 +1,127 @@
+package org.whispersystems.signalservice.internal.registrationpin;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.whispersystems.signalservice.internal.util.Hex;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Parameterized.class)
+public final class PinStretchTest {
+
+ private final String pin;
+ private final byte[] expectedStretchedPin;
+ private final byte[] expectedKeyPin1;
+ private final byte[] pinKey2;
+ private final byte[] expectedMasterKey;
+ private final String expectedRegistrationLock;
+ private final byte[] expectedKbsAccessKey;
+
+ @Parameterized.Parameters
+ public static Collection