mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Add internal pre-alpha support for Registration Lock v2.
This commit is contained in:
committed by
Greyson Parrelli
parent
058c25808b
commit
7f8ca58762
@@ -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'
|
||||
}
|
||||
|
||||
75
libsignal/service/protobuf/KeyBackupService.proto
Normal file
75
libsignal/service/protobuf/KeyBackupService.proto
Normal file
@@ -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 {
|
||||
}
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String mrenclave)
|
||||
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> 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<RemoteAttestationResponse, List<String>> 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<String> addressBook = new LinkedList<>();
|
||||
String authorization = pushServiceSocket.getContactDiscoveryAuthorization();
|
||||
RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization);
|
||||
List<String> 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<String> addressBookIterator = addressBook.iterator();
|
||||
List<String> results = new LinkedList<>();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> addressBook, RemoteAttestation remoteAttestation) {
|
||||
public static DiscoveryRequest createDiscoveryRequest(List<String> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> cookies;
|
||||
|
||||
public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys) {
|
||||
public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys, List<String> 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<String> getCookies() {
|
||||
return cookies;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,15 @@ public class SigningCertificate {
|
||||
{
|
||||
try {
|
||||
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||
Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) certificateFactory.generateCertificates(new ByteArrayInputStream(URLDecoder.decode(certificateChain).getBytes()));
|
||||
Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) certificateFactory.generateCertificates(new ByteArrayInputStream(certificateChain.getBytes()));
|
||||
List<X509Certificate> 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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) + "}";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)) + "}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RemoteAttestationResponse, List<String>> getContactDiscoveryRemoteAttestation(String authorization, RemoteAttestationRequest request, String mrenclave)
|
||||
throws IOException
|
||||
{
|
||||
Response response = makeContactDiscoveryRequest(authorization, new LinkedList<String>(), "/v1/attestation/" + mrenclave, "PUT", JsonUtil.toJson(request));
|
||||
ResponseBody body = response.body();
|
||||
List<String> rawCookies = response.headers("Set-Cookie");
|
||||
List<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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 }
|
||||
}
|
||||
|
||||
@@ -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<RemoteAttestationResponse, List<String>> attestationResponsePair = getRemoteAttestation(socket, clientSet, authorization, attestationRequest, enclaveName);
|
||||
RemoteAttestationResponse attestationResponse = attestationResponsePair.first();
|
||||
List<String> 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<RemoteAttestationResponse, List<String>> getRemoteAttestation(PushServiceSocket socket,
|
||||
PushServiceSocket.ClientSet clientSet,
|
||||
String authorization,
|
||||
RemoteAttestationRequest request,
|
||||
String enclaveName)
|
||||
throws IOException
|
||||
{
|
||||
Response response = socket.makeRequest(clientSet, authorization, new LinkedList<String>(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(request));
|
||||
ResponseBody body = response.body();
|
||||
List<String> rawCookies = response.headers("Set-Cookie");
|
||||
List<String> 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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.whispersystems.signalservice.internal.registrationpin;
|
||||
|
||||
public final class InvalidPinException extends Exception {
|
||||
|
||||
InvalidPinException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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<Object[]> data() {
|
||||
return Arrays.asList(new Object[]{
|
||||
"12345",
|
||||
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
|
||||
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
|
||||
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
|
||||
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
|
||||
},
|
||||
new Object[]{
|
||||
"12345",
|
||||
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
|
||||
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"01198dc427cbf9c6b47f344654d75a263e53b992db73be44b201f357d072dc38",
|
||||
"bd1f4e129cc705c26c2fcebd3fbc6e7db60caade89e6c465c68ed60aeedbb0c3",
|
||||
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
|
||||
},
|
||||
new Object[]{
|
||||
"١٢٣٤٥",
|
||||
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
|
||||
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
|
||||
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
|
||||
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
|
||||
},
|
||||
new Object[]{
|
||||
"9876543210",
|
||||
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
|
||||
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"127a435c15be2528f4b735423f8ee558b789e8ea1f6fe64d144d5b21a87c4e06",
|
||||
"348d327acb823b54a988cf6bea647a154e21da25cbb121a115c13b871dccd548",
|
||||
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
|
||||
},
|
||||
new Object[]{
|
||||
"9876543210",
|
||||
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
|
||||
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"128833dbde1af3da703852b6b5a845e226fe9c7e069427b9c1e41279c0cdfb3a",
|
||||
"6be0b17899cfb5c4316b92acc7db3b6a2fa5b9a19ef3e58a1c84a4de49230aa6",
|
||||
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
|
||||
},
|
||||
new Object[]{
|
||||
"0123",
|
||||
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
|
||||
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
|
||||
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
|
||||
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
|
||||
},
|
||||
new Object[]{
|
||||
"௦௧௨௩",
|
||||
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
|
||||
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
|
||||
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
|
||||
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
|
||||
});
|
||||
}
|
||||
|
||||
public PinStretchTest(String pin,
|
||||
String expectedStretchedPin,
|
||||
String expectedKeyPin1,
|
||||
String pinKey2,
|
||||
String expectedMasterKey,
|
||||
String expectedRegistrationLock,
|
||||
String expectedKbsAccessKey) throws IOException {
|
||||
this.pin = pin;
|
||||
this.expectedStretchedPin = Hex.fromStringCondensed(expectedStretchedPin);
|
||||
this.expectedKeyPin1 = Hex.fromStringCondensed(expectedKeyPin1);
|
||||
this.pinKey2 = Hex.fromStringCondensed(pinKey2);
|
||||
this.expectedMasterKey = Hex.fromStringCondensed(expectedMasterKey);
|
||||
this.expectedRegistrationLock = expectedRegistrationLock;
|
||||
this.expectedKbsAccessKey = Hex.fromStringCondensed(expectedKbsAccessKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretch_pin() throws InvalidPinException {
|
||||
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
|
||||
|
||||
assertArrayEquals(expectedStretchedPin, stretchedPin.getStretchedPin());
|
||||
assertArrayEquals(expectedKeyPin1, stretchedPin.getPinKey1());
|
||||
assertArrayEquals(expectedKbsAccessKey, stretchedPin.getKbsAccessKey());
|
||||
|
||||
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(pinKey2);
|
||||
|
||||
assertArrayEquals(pinKey2, masterKey.getPinKey2());
|
||||
assertArrayEquals(expectedMasterKey, masterKey.getMasterKey());
|
||||
assertEquals(expectedRegistrationLock, masterKey.getRegistrationLock());
|
||||
|
||||
assertArrayEquals(expectedStretchedPin, masterKey.getStretchedPin());
|
||||
assertArrayEquals(expectedKeyPin1, masterKey.getPinKey1());
|
||||
assertArrayEquals(expectedKbsAccessKey, masterKey.getKbsAccessKey());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user