From 7f8ca58762670d444daa8f2995213fcfde3fbea4 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Tue, 3 Dec 2019 12:31:23 -0500 Subject: [PATCH] Add internal pre-alpha support for Registration Lock v2. --- app/build.gradle | 5 + libsignal/service/build.gradle | 2 +- .../service/protobuf/KeyBackupService.proto | 75 +++++ .../signalservice/api/KeyBackupService.java | 281 ++++++++++++++++++ .../api/KeyBackupServicePinException.java | 17 ++ .../api/RegistrationLockData.java | 27 ++ .../api/SignalServiceAccountManager.java | 83 +++--- .../signalservice/api/TokenException.java | 22 ++ .../SignalKeyBackupServiceUrl.java | 17 ++ .../SignalServiceConfiguration.java | 11 +- .../internal/contacts/crypto/AESCipher.java | 69 +++++ .../crypto/ContactDiscoveryCipher.java | 134 +-------- .../contacts/crypto/KeyBackupCipher.java | 128 ++++++++ .../contacts/crypto/RemoteAttestation.java | 10 +- .../crypto/RemoteAttestationCipher.java | 92 ++++++ .../contacts/crypto/SigningCertificate.java | 6 +- .../contacts/entities/DiscoveryRequest.java | 2 +- .../contacts/entities/KeyBackupRequest.java | 51 ++++ .../contacts/entities/KeyBackupResponse.java | 41 +++ .../contacts/entities/TokenResponse.java | 38 +++ .../internal/push/AccountAttributes.java | 10 +- .../internal/push/AuthCredentials.java | 18 ++ .../push/ContactDiscoveryCredentials.java | 28 -- .../internal/push/LockedException.java | 18 +- .../internal/push/PushServiceSocket.java | 147 ++++++--- .../internal/push/RemoteAttestationUtil.java | 79 +++++ .../registrationpin/InvalidPinException.java | 8 + .../registrationpin/PinStretcher.java | 166 +++++++++++ .../signalservice/internal/util/Util.java | 11 +- .../api/crypto/SigningCertificateTest.java | 7 +- .../PinStretchFailureTest.java | 31 ++ .../registrationpin/PinStretchTest.java | 127 ++++++++ res/values/strings.xml | 4 + .../dependencies/ApplicationDependencies.java | 12 + .../securesms/jobs/JobManagerFactories.java | 3 + .../securesms/jobs/RefreshAttributesJob.java | 19 +- .../lock/RegistrationLockDialog.java | 163 ++++++++-- .../lock/RegistrationLockReminders.java | 4 +- .../RegistrationPinV2MigrationJob.java | 111 +++++++ .../AppProtectionPreferenceFragment.java | 14 +- .../push/SignalServiceNetworkAccess.java | 19 +- .../fragments/EnterCodeFragment.java | 12 +- .../fragments/RegistrationLockFragment.java | 71 ++++- .../service/CodeVerificationRequest.java | 180 ++++++++--- .../KeyBackupSystemWrongPinException.java | 18 ++ .../service/RegistrationService.java | 12 +- .../viewmodel/RegistrationViewModel.java | 86 ++++++ .../securesms/util/FeatureFlags.java | 3 + .../securesms/util/IasKeyStore.java | 33 ++ .../securesms/util/TextSecurePreferences.java | 128 +++++++- 50 files changed, 2313 insertions(+), 340 deletions(-) create mode 100644 libsignal/service/protobuf/KeyBackupService.proto create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupServicePinException.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/TokenException.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalKeyBackupServiceUrl.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/AESCipher.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/KeyBackupCipher.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupRequest.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupResponse.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/TokenResponse.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AuthCredentials.java delete mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ContactDiscoveryCredentials.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteAttestationUtil.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java create mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java create mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java create mode 100644 src/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java create mode 100644 src/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java create mode 100644 src/org/thoughtcrime/securesms/util/IasKeyStore.java diff --git a/app/build.gradle b/app/build.gradle index 41c82319ab..364335b9b7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -230,11 +230,14 @@ android { buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"" buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\"" buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"" + buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"" buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" buildConfigField "int", "CONTENT_PROXY_PORT", "443" buildConfigField "String", "USER_AGENT", "\"OWA\"" buildConfigField "boolean", "DEV_BUILD", "false" buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\"" + buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"f2e2a5004794a6c1bac5c4949eadbc243dd02e02d1a93f10fe24584fb70815d8\"" + buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"f51f435802ada769e67aaf5744372bb7e7d519eecf996d335eb5b46b872b5789\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"" buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" @@ -301,7 +304,9 @@ android { buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\"" buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\"" buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\"" + buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"" buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\"" + buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"b5a865941f95887018c86725cc92308d34a3084dc2b4e7bd2de5e5e1690b50c6\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"" } release { diff --git a/libsignal/service/build.gradle b/libsignal/service/build.gradle index 434f17e637..6be238d254 100644 --- a/libsignal/service/build.gradle +++ b/libsignal/service/build.gradle @@ -35,7 +35,7 @@ dependencies { api 'com.squareup.okhttp3:okhttp:3.12.1' implementation 'org.threeten:threetenbp:1.3.6' - testImplementation 'junit:junit:3.8.2' + testImplementation 'junit:junit:4.12' testImplementation 'org.assertj:assertj-core:1.7.1' testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.0.0' } diff --git a/libsignal/service/protobuf/KeyBackupService.proto b/libsignal/service/protobuf/KeyBackupService.proto new file mode 100644 index 0000000000..a339fda9fc --- /dev/null +++ b/libsignal/service/protobuf/KeyBackupService.proto @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2019 Open Whisper Systems + * + * Licensed according to the LICENSE file in this repository. + */ +syntax = "proto2"; + +package textsecure; + +option java_package = "org.whispersystems.signalservice.internal.keybackup.protos"; +option java_multiple_files = true; + +message Request { + optional BackupRequest backup = 1; + optional RestoreRequest restore = 2; + optional DeleteRequest delete = 3; +} + +message Response { + optional BackupResponse backup = 1; + optional RestoreResponse restore = 2; + optional DeleteResponse delete = 3; +} + +message BackupRequest { + optional bytes service_id = 1; + optional bytes backup_id = 2; + optional bytes token = 3; + optional uint64 valid_from = 4; + optional bytes data = 5; + optional bytes pin = 6; + optional uint32 tries = 7; +} + +message BackupResponse { + enum Status { + OK = 1; + ALREADY_EXISTS = 2; + NOT_YET_VALID = 3; + } + + optional Status status = 1; + optional bytes token = 2; +} + +message RestoreRequest { + optional bytes service_id = 1; + optional bytes backup_id = 2; + optional bytes token = 3; + optional uint64 valid_from = 4; + optional bytes pin = 5; +} + +message RestoreResponse { + enum Status { + OK = 1; + TOKEN_MISMATCH = 2; + NOT_YET_VALID = 3; + MISSING = 4; + PIN_MISMATCH = 5; + } + + optional Status status = 1; + optional bytes token = 2; + optional bytes data = 3; + optional uint32 tries = 4; +} + +message DeleteRequest { + optional bytes service_id = 1; + optional bytes backup_id = 2; +} + +message DeleteResponse { +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java new file mode 100644 index 0000000000..58ad5096a1 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java @@ -0,0 +1,281 @@ +package org.whispersystems.signalservice.api; + +import org.whispersystems.libsignal.logging.Log; +import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; +import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher; +import org.whispersystems.signalservice.internal.contacts.crypto.Quote; +import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest; +import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse; +import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse; +import org.whispersystems.signalservice.internal.push.PushServiceSocket; +import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil; +import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException; +import org.whispersystems.signalservice.internal.registrationpin.PinStretcher; +import org.whispersystems.signalservice.internal.util.Hex; +import org.whispersystems.signalservice.internal.util.Util; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.SignatureException; +import java.util.Locale; + +public final class KeyBackupService { + + private static final String TAG = KeyBackupService.class.getSimpleName(); + + private final KeyStore iasKeyStore; + private final String enclaveName; + private final String mrenclave; + private final PushServiceSocket pushServiceSocket; + private final int maxTries; + + KeyBackupService(KeyStore iasKeyStore, + String enclaveName, + String mrenclave, + PushServiceSocket pushServiceSocket, + int maxTries) + { + this.iasKeyStore = iasKeyStore; + this.enclaveName = enclaveName; + this.mrenclave = mrenclave; + this.pushServiceSocket = pushServiceSocket; + this.maxTries = maxTries; + } + + /** + * Use this if you don't want to validate that the server has not changed since you last set the pin. + */ + public PinChangeSession newPinChangeSession() + throws IOException + { + return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), null); + } + + /** + * Use this if you want to validate that the server has not changed since you last set the pin. + * The supplied token will have to match for the change to be successful. + */ + public PinChangeSession newPinChangeSession(TokenResponse currentToken) + throws IOException + { + return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken); + } + + /** + * Use this to validate that the pin is still set on the server with the current token. + * Additionally this validates that no one has used any tries. + */ + public RestoreSession newRestoreSession(TokenResponse currentToken) + throws IOException + { + return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken); + } + + /** + * Only call before registration, to see how many tries are left. + *

+ * Pass the token to the newRegistrationSession. + */ + public TokenResponse getToken(String authAuthorization) throws IOException { + return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName); + } + + /** + * Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt. + * + * @param tokenResponse Supplying a token response from a failed previous attempt prevents certain attacks. + */ + public RestoreSession newRegistrationSession(String authAuthorization, TokenResponse tokenResponse) + throws IOException + { + return newSession(authAuthorization, tokenResponse); + } + + private Session newSession(String authorization, TokenResponse currentToken) + throws IOException + { + TokenResponse token = currentToken != null ? currentToken : pushServiceSocket.getKeyBackupServiceToken(authorization, enclaveName); + + return new Session(authorization, token); + } + + private class Session implements RestoreSession, PinChangeSession { + + private final String authorization; + private final TokenResponse currentToken; + + Session(String authorization, TokenResponse currentToken) { + this.authorization = authorization; + this.currentToken = currentToken; + } + + @Override + public RegistrationLockData restorePin(String pin) + throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException + { + int attempt = 0; + SecureRandom random = new SecureRandom(); + TokenResponse token = currentToken; + + while (true) { + + attempt++; + + try { + return restorePin(pin, token); + } catch (TokenException tokenException) { + + token = tokenException.getToken(); + + if (tokenException instanceof KeyBackupServicePinException) { + throw (KeyBackupServicePinException) tokenException; + } + + if (tokenException.isCanAutomaticallyRetry() && attempt < 5) { + // back off randomly, between 250 and 8000 ms + int backoffMs = 250 * (1 << (attempt - 1)); + + Util.sleep(backoffMs + random.nextInt(backoffMs)); + } else { + throw new UnauthenticatedResponseException("Token mismatch, expended all automatic retries"); + } + } + } + } + + private RegistrationLockData restorePin(String pin, TokenResponse token) + throws UnauthenticatedResponseException, IOException, TokenException, InvalidPinException + { + PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin); + + try { + final int remainingTries = token.getTries(); + final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation(); + final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(stretchedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName)); + final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName); + final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation); + + TokenResponse nextToken = status.hasToken() + ? new TokenResponse(token.getBackupId(), status.getToken().toByteArray(), status.getTries()) + : token; + + Log.i(TAG, "Restore " + status.getStatus()); + switch (status.getStatus()) { + case OK: + Log.i(TAG, String.format(Locale.US,"Restore OK! data: %s tries: %d", Hex.toStringCondensed(status.getData().toByteArray()), status.getTries())); + PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(status.getData().toByteArray()); + return new RegistrationLockData(masterKey, nextToken); + case PIN_MISMATCH: + Log.i(TAG, "Restore PIN_MISMATCH"); + throw new KeyBackupServicePinException(nextToken); + case TOKEN_MISMATCH: + Log.i(TAG, "Restore TOKEN_MISMATCH"); + // if the number of tries has not fallen, the pin is correct we're just using an out of date token + boolean canRetry = remainingTries == status.getTries(); + Log.i(TAG, String.format(Locale.US, "Token MISMATCH %d %d", remainingTries, status.getTries())); + Log.i(TAG, String.format("Last token %s", Hex.toStringCondensed(token.getToken()))); + Log.i(TAG, String.format("Next token %s", Hex.toStringCondensed(nextToken.getToken()))); + throw new TokenException(nextToken, canRetry); + case MISSING: + Log.i(TAG, "Restore OK! No data though"); + return null; + case NOT_YET_VALID: + throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch"); + } + } catch (InvalidCiphertextException e) { + throw new UnauthenticatedResponseException(e); + } + return null; + } + + private RemoteAttestation getAndVerifyRemoteAttestation() throws UnauthenticatedResponseException, IOException { + try { + return RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.KeyBackup, iasKeyStore, enclaveName, mrenclave, authorization); + } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | InvalidCiphertextException | SignatureException e) { + throw new UnauthenticatedResponseException(e); + } + } + + @Override + public RegistrationLockData setPin(String pin) throws IOException, UnauthenticatedResponseException, InvalidPinException { + PinStretcher.MasterKey masterKey = PinStretcher.stretchPin(pin) + .withNewSecurePinKey2(); + + TokenResponse tokenResponse = putKbsData(masterKey.getKbsAccessKey(), + masterKey.getPinKey2(), + enclaveName, + currentToken); + + pushServiceSocket.setRegistrationLock(masterKey.getRegistrationLock()); + + return new RegistrationLockData(masterKey, tokenResponse); + } + + @Override + public void removePin() throws IOException, UnauthenticatedResponseException { + deleteKbsData(); + + pushServiceSocket.removePinV2(); + } + + private TokenResponse putKbsData(byte[] kbsAccessKey, byte[] kbsData, String enclaveName, TokenResponse token) + throws IOException, UnauthenticatedResponseException + { + try { + RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation(); + KeyBackupRequest request = KeyBackupCipher.createKeyBackupRequest(kbsAccessKey, kbsData, token, remoteAttestation, Hex.fromStringCondensed(enclaveName), maxTries); + KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName); + BackupResponse backupResponse = KeyBackupCipher.getKeyBackupResponse(response, remoteAttestation); + BackupResponse.Status status = backupResponse.getStatus(); + + switch (status) { + case OK: + return backupResponse.hasToken() ? new TokenResponse(token.getBackupId(), backupResponse.getToken().toByteArray(), maxTries) : token; + case ALREADY_EXISTS: + throw new UnauthenticatedResponseException("Already exists"); + case NOT_YET_VALID: + throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch"); + default: + throw new AssertionError("Unknown response status " + status); + } + } catch (InvalidCiphertextException e) { + throw new UnauthenticatedResponseException(e); + } + } + + private void deleteKbsData() + throws IOException, UnauthenticatedResponseException + { + try { + RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation(); + KeyBackupRequest request = KeyBackupCipher.createKeyDeleteRequest(currentToken, remoteAttestation, Hex.fromStringCondensed(enclaveName)); + KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName); + + KeyBackupCipher.getKeyDeleteResponseStatus(response, remoteAttestation); + } catch (InvalidCiphertextException e) { + throw new UnauthenticatedResponseException(e); + } + } + } + + public interface RestoreSession { + + RegistrationLockData restorePin(String pin) + throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException; + } + + public interface PinChangeSession { + + RegistrationLockData setPin(String pin) + throws IOException, UnauthenticatedResponseException, InvalidPinException; + + void removePin() + throws IOException, UnauthenticatedResponseException; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupServicePinException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupServicePinException.java new file mode 100644 index 0000000000..79668ebde6 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupServicePinException.java @@ -0,0 +1,17 @@ +package org.whispersystems.signalservice.api; + +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; + +public final class KeyBackupServicePinException extends TokenException { + + private final int triesRemaining; + + public KeyBackupServicePinException(TokenResponse nextToken) { + super(nextToken, false); + this.triesRemaining = nextToken.getTries(); + } + + public int getTriesRemaining() { + return triesRemaining; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java new file mode 100644 index 0000000000..741a51651f --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/RegistrationLockData.java @@ -0,0 +1,27 @@ +package org.whispersystems.signalservice.api; + +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.registrationpin.PinStretcher; + +public final class RegistrationLockData { + + private final PinStretcher.MasterKey masterKey; + private final TokenResponse tokenResponse; + + RegistrationLockData(PinStretcher.MasterKey masterKey, TokenResponse tokenResponse) { + this.masterKey = masterKey; + this.tokenResponse = tokenResponse; + } + + public PinStretcher.MasterKey getMasterKey() { + return masterKey; + } + + public TokenResponse getTokenResponse() { + return tokenResponse; + } + + public int getRemainingTries() { + return tokenResponse.getTries(); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 873d23ad1f..d6606efbb4 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -9,8 +9,6 @@ package org.whispersystems.signalservice.api; import com.google.protobuf.ByteString; -import org.whispersystems.curve25519.Curve25519; -import org.whispersystems.curve25519.Curve25519KeyPair; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -18,7 +16,6 @@ import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.logging.Log; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; @@ -33,23 +30,19 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher; import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation; -import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest; import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse; -import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest; -import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse; import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher; import org.whispersystems.signalservice.internal.push.ProfileAvatarData; import org.whispersystems.signalservice.internal.push.PushServiceSocket; +import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil; import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory; import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider; import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.util.Base64; -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; import java.io.IOException; import java.security.KeyStore; import java.security.MessageDigest; @@ -117,18 +110,34 @@ public class SignalServiceAccountManager { return this.pushServiceSocket.getSenderCertificateLegacy(); } - public void setPin(Optional pin) throws IOException { - if (pin.isPresent()) { - this.pushServiceSocket.setPin(pin.get()); - } else { - this.pushServiceSocket.removePin(); - } + /** + * @deprecated Remove this method once KBS is live. + */ + @Deprecated + public void setPin(String pin) throws IOException { + this.pushServiceSocket.setPin(pin); + } + + /** + * V1 Pin setting has been replaced by KeyBackupService. + * Now you can only remove the old pin but there is no need to remove the old pin if setting a KBS Pin. + */ + public void removeV1Pin() throws IOException { + this.pushServiceSocket.removePin(); } public UUID getOwnUuid() throws IOException { return this.pushServiceSocket.getOwnUuid(); } + public KeyBackupService getKeyBackupService(KeyStore iasKeyStore, + String enclaveName, + String mrenclave, + int tries) + { + return new KeyBackupService(iasKeyStore, enclaveName, mrenclave, pushServiceSocket, tries); + } + /** * Register/Unregister a Google Cloud Messaging registration ID. * @@ -193,16 +202,20 @@ public class SignalServiceAccountManager { * This value should remain consistent across registrations for the * same install, but probabilistically differ across registrations * for separate installs. + * @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS. + * @param registrationLock Only supply if found on KBS. * @return The UUID of the user that was registered. * @throws IOException */ - public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin, - byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) + public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, + String pin, String registrationLock, + byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) throws IOException { return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey, signalProtocolRegistrationId, - fetchesMessages, pin, + fetchesMessages, + pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess); } @@ -215,14 +228,18 @@ public class SignalServiceAccountManager { * This value should remain consistent across registrations for the same * install, but probabilistically differ across registrations for * separate installs. + * @param pin Only supply if pin has not yet been migrated to KBS. + * @param registrationLock Only supply if found on KBS. * * @throws IOException */ - public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin, + public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, + String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) throws IOException { - this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages, pin, + this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages, + pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess); } @@ -306,35 +323,21 @@ public class SignalServiceAccountManager { return activeTokens; } - public List getRegisteredUsers(KeyStore iasKeyStore, Set e164numbers, String mrenclave) + public List getRegisteredUsers(KeyStore iasKeyStore, Set e164numbers, String enclaveId) throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException { try { - String authorization = this.pushServiceSocket.getContactDiscoveryAuthorization(); - Curve25519 curve = Curve25519.getInstance(Curve25519.BEST); - Curve25519KeyPair keyPair = curve.generateKeyPair(); - - ContactDiscoveryCipher cipher = new ContactDiscoveryCipher(); - RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey()); - Pair> attestationResponse = this.pushServiceSocket.getContactDiscoveryRemoteAttestation(authorization, attestationRequest, mrenclave); - - RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.first().getServerEphemeralPublic(), attestationResponse.first().getServerStaticPublic()); - Quote quote = new Quote(attestationResponse.first().getQuote()); - byte[] requestId = cipher.getRequestId(keys, attestationResponse.first()); - - cipher.verifyServerQuote(quote, attestationResponse.first().getServerStaticPublic(), mrenclave); - cipher.verifyIasSignature(iasKeyStore, attestationResponse.first().getCertificates(), attestationResponse.first().getSignatureBody(), attestationResponse.first().getSignature(), quote); - - RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys); - List addressBook = new LinkedList<>(); + String authorization = pushServiceSocket.getContactDiscoveryAuthorization(); + RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization); + List addressBook = new LinkedList<>(); for (String e164number : e164numbers) { addressBook.add(e164number.substring(1)); } - DiscoveryRequest request = cipher.createDiscoveryRequest(addressBook, remoteAttestation); - DiscoveryResponse response = this.pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, attestationResponse.second(), mrenclave); - byte[] data = cipher.getDiscoveryResponseData(response, remoteAttestation); + DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, remoteAttestation); + DiscoveryResponse response = pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, remoteAttestation.getCookies(), enclaveId); + byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, remoteAttestation); Iterator addressBookIterator = addressBook.iterator(); List results = new LinkedList<>(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/TokenException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/TokenException.java new file mode 100644 index 0000000000..baea0c0cb8 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/TokenException.java @@ -0,0 +1,22 @@ +package org.whispersystems.signalservice.api; + +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; + +class TokenException extends Exception { + + private final TokenResponse nextToken; + private final boolean canAutomaticallyRetry; + + TokenException(TokenResponse nextToken, boolean canAutomaticallyRetry) { + this.nextToken = nextToken; + this.canAutomaticallyRetry = canAutomaticallyRetry; + } + + public TokenResponse getToken() { + return nextToken; + } + + public boolean isCanAutomaticallyRetry() { + return canAutomaticallyRetry; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalKeyBackupServiceUrl.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalKeyBackupServiceUrl.java new file mode 100644 index 0000000000..2dfc6717ad --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalKeyBackupServiceUrl.java @@ -0,0 +1,17 @@ +package org.whispersystems.signalservice.internal.configuration; + + +import org.whispersystems.signalservice.api.push.TrustStore; + +import okhttp3.ConnectionSpec; + +public class SignalKeyBackupServiceUrl extends SignalUrl { + + public SignalKeyBackupServiceUrl(String url, TrustStore trustStore) { + super(url, trustStore); + } + + public SignalKeyBackupServiceUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) { + super(url, hostHeader, trustStore, connectionSpec); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java index 86806cd0c8..fdd528acac 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java @@ -6,11 +6,16 @@ public class SignalServiceConfiguration { private final SignalServiceUrl[] signalServiceUrls; private final SignalCdnUrl[] signalCdnUrls; private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls; + private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls; - public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, SignalCdnUrl[] signalCdnUrls, SignalContactDiscoveryUrl[] signalContactDiscoveryUrls) { + public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, + SignalCdnUrl[] signalCdnUrls, + SignalContactDiscoveryUrl[] signalContactDiscoveryUrls, + SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls) { this.signalServiceUrls = signalServiceUrls; this.signalCdnUrls = signalCdnUrls; this.signalContactDiscoveryUrls = signalContactDiscoveryUrls; + this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls; } public SignalServiceUrl[] getSignalServiceUrls() { @@ -24,4 +29,8 @@ public class SignalServiceConfiguration { public SignalContactDiscoveryUrl[] getSignalContactDiscoveryUrls() { return signalContactDiscoveryUrls; } + + public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() { + return signalKeyBackupServiceUrls; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/AESCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/AESCipher.java new file mode 100644 index 0000000000..19b7fdc4a6 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/AESCipher.java @@ -0,0 +1,69 @@ +package org.whispersystems.signalservice.internal.contacts.crypto; + +import org.whispersystems.libsignal.util.ByteUtil; +import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; +import org.whispersystems.signalservice.internal.util.Util; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +final class AESCipher { + + private static final int TAG_LENGTH_BYTES = 16; + private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8; + + static byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException { + try { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, iv)); + + return cipher.doFinal(ByteUtil.combine(ciphertext, tag)); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) { + throw new AssertionError(e); + } catch (InvalidKeyException | BadPaddingException e) { + throw new InvalidCiphertextException(e); + } + } + + static AESEncryptedResult encrypt(byte[] key, byte[] aad, byte[] requestData) { + try { + byte[] iv = Util.getSecretBytes(12); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, iv)); + cipher.updateAAD(aad); + + byte[] cipherText = cipher.doFinal(requestData); + byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES); + + byte[] mac = parts[1]; + byte[] data = parts[0]; + + return new AESEncryptedResult(iv, data, mac, aad); + } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { + throw new AssertionError(e); + } + } + + static class AESEncryptedResult { + final byte[] iv; + final byte[] data; + final byte[] mac; + final byte[] aad; + + private AESEncryptedResult(byte[] iv, byte[] data, byte[] mac, byte[] aad) { + this.iv = iv; + this.data = data; + this.mac = mac; + this.aad = aad; + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/ContactDiscoveryCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/ContactDiscoveryCipher.java index 1e3e46efc5..d2cf0d2087 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/ContactDiscoveryCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/ContactDiscoveryCipher.java @@ -1,47 +1,20 @@ package org.whispersystems.signalservice.internal.contacts.crypto; - -import org.threeten.bp.Instant; -import org.threeten.bp.LocalDateTime; -import org.threeten.bp.Period; -import org.threeten.bp.ZoneId; -import org.threeten.bp.ZonedDateTime; -import org.threeten.bp.format.DateTimeFormatter; import org.whispersystems.libsignal.util.ByteUtil; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest; import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse; -import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse; -import org.whispersystems.signalservice.internal.util.Hex; -import org.whispersystems.signalservice.internal.util.JsonUtil; -import org.whispersystems.signalservice.internal.util.Util; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.KeyStore; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.security.cert.CertPathValidatorException; -import java.security.cert.CertificateException; import java.util.List; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.SecretKeySpec; +public final class ContactDiscoveryCipher { -public class ContactDiscoveryCipher { + private ContactDiscoveryCipher() { + } - private static final int TAG_LENGTH_BYTES = 16; - private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8; - private static final long SIGNATURE_BODY_VERSION = 3L; - - public DiscoveryRequest createDiscoveryRequest(List addressBook, RemoteAttestation remoteAttestation) { + public static DiscoveryRequest createDiscoveryRequest(List addressBook, RemoteAttestation remoteAttestation) { try { ByteArrayOutputStream requestDataStream = new ByteArrayOutputStream(); @@ -49,100 +22,19 @@ public class ContactDiscoveryCipher { requestDataStream.write(ByteUtil.longToByteArray(Long.parseLong(address))); } - byte[] requestData = requestDataStream.toByteArray(); - byte[] nonce = Util.getSecretBytes(12); - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + byte[] clientKey = remoteAttestation.getKeys().getClientKey(); + byte[] requestData = requestDataStream.toByteArray(); + byte[] aad = remoteAttestation.getRequestId(); - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(remoteAttestation.getKeys().getClientKey(), "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, nonce)); - cipher.updateAAD(remoteAttestation.getRequestId()); + AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData); - byte[] cipherText = cipher.doFinal(requestData); - byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES); - - return new DiscoveryRequest(addressBook.size(), remoteAttestation.getRequestId(), nonce, parts[0], parts[1]); - } catch (IOException | NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { - throw new AssertionError(e); - } - } - - public byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException { - return decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac()); - } - - public byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException { - return decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag()); - } - - public void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave) - throws UnauthenticatedQuoteException - { - try { - byte[] theirServerPublicStatic = new byte[serverPublicStatic.length]; - System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length); - - if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) { - throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!"); - } - - if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) { - throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave())); - } - - if (quote.isDebugQuote()) { - throw new UnauthenticatedQuoteException("Received quote for debuggable enclave"); - } + return new DiscoveryRequest(addressBook.size(), aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac); } catch (IOException e) { - throw new UnauthenticatedQuoteException(e); - } - } - - public void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote) - throws SignatureException - { - if (certificates == null || certificates.isEmpty()) { - throw new SignatureException("No certificates."); - } - - try { - SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore); - signingCertificate.verifySignature(signatureBody, signature); - - SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class); - - if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) { - throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion()); - } - - if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) { - throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes())); - } - - if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) { - throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus()); - } - - if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC"))) - .plus(Period.ofDays(1)) - .isBefore(Instant.now())) - { - throw new SignatureException("Signature is expired"); - } - - } catch (CertificateException | CertPathValidatorException | IOException e) { - throw new SignatureException(e); - } - } - - private byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException { - try { - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv)); - - return cipher.doFinal(ByteUtil.combine(ciphertext, tag)); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) { throw new AssertionError(e); - } catch (InvalidKeyException | BadPaddingException e) { - throw new InvalidCiphertextException(e); } } + + public static byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException { + return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac()); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/KeyBackupCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/KeyBackupCipher.java new file mode 100644 index 0000000000..b7a16f3ad2 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/KeyBackupCipher.java @@ -0,0 +1,128 @@ +package org.whispersystems.signalservice.internal.contacts.crypto; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; + +import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; +import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest; +import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.keybackup.protos.BackupRequest; +import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse; +import org.whispersystems.signalservice.internal.keybackup.protos.DeleteRequest; +import org.whispersystems.signalservice.internal.keybackup.protos.DeleteResponse; +import org.whispersystems.signalservice.internal.keybackup.protos.Request; +import org.whispersystems.signalservice.internal.keybackup.protos.Response; +import org.whispersystems.signalservice.internal.keybackup.protos.RestoreRequest; +import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse; + +import java.util.concurrent.TimeUnit; + +public final class KeyBackupCipher { + + private KeyBackupCipher() { + } + + private static final long VALID_FROM_BUFFER_MS = TimeUnit.DAYS.toMillis(1); + + public static KeyBackupRequest createKeyBackupRequest(byte[] kbsAccessKey, + byte[] kbsData, + TokenResponse token, + RemoteAttestation remoteAttestation, + byte[] serviceId, + int tries) + { + long now = System.currentTimeMillis(); + + BackupRequest backupRequest = BackupRequest.newBuilder() + .setServiceId(ByteString.copyFrom(serviceId)) + .setBackupId(ByteString.copyFrom(token.getBackupId())) + .setToken(ByteString.copyFrom(token.getToken())) + .setValidFrom(getValidFromSeconds(now)) + .setData(ByteString.copyFrom(kbsData)) + .setPin(ByteString.copyFrom(kbsAccessKey)) + .setTries(tries) + .build(); + + Request requestData = Request.newBuilder().setBackup(backupRequest).build(); + + return createKeyBackupRequest(requestData, remoteAttestation); + } + + public static KeyBackupRequest createKeyRestoreRequest(byte[] kbsAccessKey, + TokenResponse token, + RemoteAttestation remoteAttestation, + byte[] serviceId) + { + long now = System.currentTimeMillis(); + + RestoreRequest restoreRequest = RestoreRequest.newBuilder() + .setServiceId(ByteString.copyFrom(serviceId)) + .setBackupId(ByteString.copyFrom(token.getBackupId())) + .setToken(ByteString.copyFrom(token.getToken())) + .setValidFrom(getValidFromSeconds(now)) + .setPin(ByteString.copyFrom(kbsAccessKey)) + .build(); + + Request request = Request.newBuilder().setRestore(restoreRequest).build(); + + return createKeyBackupRequest(request, remoteAttestation); + } + + public static KeyBackupRequest createKeyDeleteRequest(TokenResponse token, + RemoteAttestation remoteAttestation, + byte[] serviceId) + { + DeleteRequest deleteRequest = DeleteRequest.newBuilder() + .setServiceId(ByteString.copyFrom(serviceId)) + .setBackupId(ByteString.copyFrom(token.getBackupId())) + .build(); + + Request request = Request.newBuilder().setDelete(deleteRequest).build(); + + return createKeyBackupRequest(request, remoteAttestation); + } + + public static BackupResponse getKeyBackupResponse(KeyBackupResponse response, RemoteAttestation remoteAttestation) + throws InvalidCiphertextException, InvalidProtocolBufferException + { + byte[] data = decryptData(response, remoteAttestation); + + Response backupResponse = Response.parseFrom(data); + + return backupResponse.getBackup(); + } + + public static RestoreResponse getKeyRestoreResponse(KeyBackupResponse response, RemoteAttestation remoteAttestation) + throws InvalidCiphertextException, InvalidProtocolBufferException + { + byte[] data = decryptData(response, remoteAttestation); + + return Response.parseFrom(data).getRestore(); + } + + public static DeleteResponse getKeyDeleteResponseStatus(KeyBackupResponse response, RemoteAttestation remoteAttestation) + throws InvalidCiphertextException, InvalidProtocolBufferException + { + byte[] data = decryptData(response, remoteAttestation); + + return DeleteResponse.parseFrom(data); + } + + private static KeyBackupRequest createKeyBackupRequest(Request requestData, RemoteAttestation remoteAttestation) { + byte[] clientKey = remoteAttestation.getKeys().getClientKey(); + byte[] aad = remoteAttestation.getRequestId(); + + AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData.toByteArray()); + + return new KeyBackupRequest(aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac); + } + + private static byte[] decryptData(KeyBackupResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException { + return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac()); + } + + private static long getValidFromSeconds(long nowMs) { + return TimeUnit.MILLISECONDS.toSeconds(nowMs - VALID_FROM_BUFFER_MS); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestation.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestation.java index 7e0c7e953c..62ad1cfdf1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestation.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestation.java @@ -1,13 +1,17 @@ package org.whispersystems.signalservice.internal.contacts.crypto; +import java.util.List; + public class RemoteAttestation { private final byte[] requestId; private final RemoteAttestationKeys keys; + private final List cookies; - public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys) { + public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys, List cookies) { this.requestId = requestId; this.keys = keys; + this.cookies = cookies; } public byte[] getRequestId() { @@ -17,4 +21,8 @@ public class RemoteAttestation { public RemoteAttestationKeys getKeys() { return keys; } + + public List getCookies() { + return cookies; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java new file mode 100644 index 0000000000..7bd3481c71 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java @@ -0,0 +1,92 @@ +package org.whispersystems.signalservice.internal.contacts.crypto; + +import org.threeten.bp.Instant; +import org.threeten.bp.LocalDateTime; +import org.threeten.bp.Period; +import org.threeten.bp.ZoneId; +import org.threeten.bp.ZonedDateTime; +import org.threeten.bp.format.DateTimeFormatter; +import org.whispersystems.libsignal.util.ByteUtil; +import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; +import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse; +import org.whispersystems.signalservice.internal.util.Hex; +import org.whispersystems.signalservice.internal.util.JsonUtil; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.SignatureException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; + +public final class RemoteAttestationCipher { + + private RemoteAttestationCipher() { + } + + private static final long SIGNATURE_BODY_VERSION = 3L; + + public static byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException { + return AESCipher.decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag()); + } + + public static void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave) + throws UnauthenticatedQuoteException + { + try { + byte[] theirServerPublicStatic = new byte[serverPublicStatic.length]; + System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length); + + if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) { + throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!"); + } + + if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) { + throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave())); + } + + if (quote.isDebugQuote()) { + throw new UnauthenticatedQuoteException("Received quote for debuggable enclave"); + } + } catch (IOException e) { + throw new UnauthenticatedQuoteException(e); + } + } + + public static void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote) + throws SignatureException + { + if (certificates == null || certificates.isEmpty()) { + throw new SignatureException("No certificates."); + } + + try { + SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore); + signingCertificate.verifySignature(signatureBody, signature); + + SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class); + + if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) { + throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion()); + } + + if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) { + throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes())); + } + + if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) { + throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus()); + } + + if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC"))) + .plus(Period.ofDays(1)) + .isBefore(Instant.now())) + { + throw new SignatureException("Signature is expired"); + } + + } catch (CertificateException | CertPathValidatorException | IOException e) { + throw new SignatureException(e); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/SigningCertificate.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/SigningCertificate.java index f26fd7605e..ad90b40cc4 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/SigningCertificate.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/SigningCertificate.java @@ -31,11 +31,15 @@ public class SigningCertificate { { try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - Collection certificatesCollection = (Collection) certificateFactory.generateCertificates(new ByteArrayInputStream(URLDecoder.decode(certificateChain).getBytes())); + Collection certificatesCollection = (Collection) certificateFactory.generateCertificates(new ByteArrayInputStream(certificateChain.getBytes())); List certificates = new LinkedList<>(certificatesCollection); PKIXParameters pkixParameters = new PKIXParameters(trustStore); CertPathValidator validator = CertPathValidator.getInstance("PKIX"); + if (certificates.isEmpty()) { + throw new CertificateException("No certificates available! Badly-formatted cert chain?"); + } + this.path = certificateFactory.generateCertPath(certificates); pkixParameters.setRevocationEnabled(false); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/DiscoveryRequest.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/DiscoveryRequest.java index e10cb9be25..3ed0259ed9 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/DiscoveryRequest.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/DiscoveryRequest.java @@ -47,7 +47,7 @@ public class DiscoveryRequest { this.requestId = requestId; this.iv = iv; this.data = data; - this. mac = mac; + this.mac = mac; } public byte[] getRequestId() { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupRequest.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupRequest.java new file mode 100644 index 0000000000..4bc5c7cde1 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupRequest.java @@ -0,0 +1,51 @@ +package org.whispersystems.signalservice.internal.contacts.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.whispersystems.signalservice.internal.util.Hex; + +public class KeyBackupRequest { + + @JsonProperty + private byte[] requestId; + + @JsonProperty + private byte[] iv; + + @JsonProperty + private byte[] data; + + @JsonProperty + private byte[] mac; + + public KeyBackupRequest() { + } + + public KeyBackupRequest(byte[] requestId, byte[] iv, byte[] data, byte[] mac) { + this.requestId = requestId; + this.iv = iv; + this.data = data; + this.mac = mac; + } + + public byte[] getRequestId() { + return requestId; + } + + public byte[] getIv() { + return iv; + } + + public byte[] getData() { + return data; + } + + public byte[] getMac() { + return mac; + } + + public String toString() { + return "{ requestId: " + Hex.toString(requestId) + ", iv: " + Hex.toString(iv) + ", data: " + Hex.toString(data) + ", mac: " + Hex.toString(mac) + "}"; + } + +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupResponse.java new file mode 100644 index 0000000000..76bb663f33 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/KeyBackupResponse.java @@ -0,0 +1,41 @@ +package org.whispersystems.signalservice.internal.contacts.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.whispersystems.signalservice.internal.util.Hex; + +public class KeyBackupResponse { + + @JsonProperty + private byte[] iv; + + @JsonProperty + private byte[] data; + + @JsonProperty + private byte[] mac; + + public KeyBackupResponse() {} + + public KeyBackupResponse(byte[] iv, byte[] data, byte[] mac) { + this.iv = iv; + this.data = data; + this.mac = mac; + } + + public byte[] getIv() { + return iv; + } + + public byte[] getData() { + return data; + } + + public byte[] getMac() { + return mac; + } + + public String toString() { + return "{iv: " + (iv == null ? null : Hex.toString(iv)) + ", data: " + (data == null ? null: Hex.toString(data)) + ", mac: " + (mac == null ? null : Hex.toString(mac)) + "}"; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/TokenResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/TokenResponse.java new file mode 100644 index 0000000000..483e705c52 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/entities/TokenResponse.java @@ -0,0 +1,38 @@ +package org.whispersystems.signalservice.internal.contacts.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TokenResponse { + + @JsonProperty + private byte[] backupId; + + @JsonProperty + private byte[] token; + + @JsonProperty + private int tries; + + @JsonCreator + public TokenResponse() { + } + + public TokenResponse(byte[] backupId, byte[] token, int tries) { + this.backupId = backupId; + this.token = token; + this.tries = tries; + } + + public byte[] getBackupId() { + return backupId; + } + + public byte[] getToken() { + return token; + } + + public int getTries() { + return tries; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java index c46141c99f..ad881637d3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java @@ -28,19 +28,23 @@ public class AccountAttributes { @JsonProperty private String pin; + @JsonProperty + private String registrationLock; + @JsonProperty private byte[] unidentifiedAccessKey; @JsonProperty private boolean unrestrictedUnidentifiedAccess; - public AccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) { + public AccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) { this.signalingKey = signalingKey; this.registrationId = registrationId; this.voice = true; this.video = true; this.fetchesMessages = fetchesMessages; this.pin = pin; + this.registrationLock = registrationLock; this.unidentifiedAccessKey = unidentifiedAccessKey; this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; } @@ -71,6 +75,10 @@ public class AccountAttributes { return pin; } + public String getRegistrationLock() { + return registrationLock; + } + public byte[] getUnidentifiedAccessKey() { return unidentifiedAccessKey; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AuthCredentials.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AuthCredentials.java new file mode 100644 index 0000000000..a6dec30bb6 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AuthCredentials.java @@ -0,0 +1,18 @@ +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import okhttp3.Credentials; + +public class AuthCredentials { + + @JsonProperty + private String username; + + @JsonProperty + private String password; + + public String asBasic() { + return Credentials.basic(username, password); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ContactDiscoveryCredentials.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ContactDiscoveryCredentials.java deleted file mode 100644 index 35e0d99022..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ContactDiscoveryCredentials.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.whispersystems.signalservice.internal.push; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class ContactDiscoveryCredentials { - - @JsonProperty - private String username; - - @JsonProperty - private String password; - - public void setUsername(String username) { - this.username = username; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java index bc48509429..37ad5701cc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java @@ -3,14 +3,16 @@ package org.whispersystems.signalservice.internal.push; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; -public class LockedException extends NonSuccessfulResponseCodeException { +public final class LockedException extends NonSuccessfulResponseCodeException { - private int length; - private long timeRemaining; + private final int length; + private final long timeRemaining; + private final String basicStorageCredentials; - LockedException(int length, long timeRemaining) { - this.length = length; - this.timeRemaining = timeRemaining; + LockedException(int length, long timeRemaining, String basicStorageCredentials) { + this.length = length; + this.timeRemaining = timeRemaining; + this.basicStorageCredentials = basicStorageCredentials; } public int getLength() { @@ -20,4 +22,8 @@ public class LockedException extends NonSuccessfulResponseCodeException { public long getTimeRemaining() { return timeRemaining; } + + public String getBasicStorageCredentials() { + return basicStorageCredentials; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index f051c4e2a0..9bf00a11ee 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -43,8 +43,9 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf import org.whispersystems.signalservice.internal.configuration.SignalUrl; import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest; import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse; -import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest; -import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse; +import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest; +import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException; import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException; import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody; @@ -78,14 +79,15 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import okhttp3.Call; import okhttp3.ConnectionSpec; -import okhttp3.Credentials; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; @@ -108,6 +110,7 @@ public class PushServiceSocket { private static final String TURN_SERVER_INFO = "/v1/accounts/turn"; private static final String SET_ACCOUNT_ATTRIBUTES = "/v1/accounts/attributes/"; private static final String PIN_PATH = "/v1/accounts/pin/"; + private static final String REGISTRATION_LOCK_PATH = "/v1/accounts/registration_lock"; private static final String REQUEST_PUSH_CHALLENGE = "/v1/accounts/fcm/preauth/%s/%s"; private static final String WHO_AM_I = "/v1/accounts/whoami"; private static final String SET_USERNAME_PATH = "/v1/accounts/username/%s"; @@ -137,6 +140,8 @@ public class PushServiceSocket { private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery"; private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true"; + private static final String KBS_AUTH_PATH = "/v1/backup/auth"; + private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d"; private static final String ATTACHMENT_UPLOAD_PATH = "attachments/"; @@ -152,6 +157,7 @@ public class PushServiceSocket { private final ServiceConnectionHolder[] serviceClients; private final ConnectionHolder[] cdnClients; private final ConnectionHolder[] contactDiscoveryClients; + private final ConnectionHolder[] keyBackupServiceClients; private final OkHttpClient attachmentClient; private final CredentialsProvider credentialsProvider; @@ -164,6 +170,7 @@ public class PushServiceSocket { this.serviceClients = createServiceConnectionHolders(signalServiceConfiguration.getSignalServiceUrls()); this.cdnClients = createConnectionHolders(signalServiceConfiguration.getSignalCdnUrls()); this.contactDiscoveryClients = createConnectionHolders(signalServiceConfiguration.getSignalContactDiscoveryUrls()); + this.keyBackupServiceClients = createConnectionHolders(signalServiceConfiguration.getSignalKeyBackupServiceUrls()); this.attachmentClient = createAttachmentClient(); this.random = new SecureRandom(); } @@ -219,11 +226,12 @@ public class PushServiceSocket { } } - public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, String pin, + public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, + String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) throws IOException { - AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, unidentifiedAccessKey, unrestrictedUnidentifiedAccess); + AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess); String requestBody = JsonUtil.toJson(signalingKeyEntity); String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody); VerifyAccountResponse response = JsonUtil.fromJson(responseBody, VerifyAccountResponse.class); @@ -236,11 +244,16 @@ public class PushServiceSocket { } } - public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, + public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, + String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) throws IOException { - AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, + if (registrationLock != null && pin != null) { + throw new AssertionError("Pin should be null if registrationLock is set."); + } + + AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess); makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes)); } @@ -282,10 +295,20 @@ public class PushServiceSocket { makeServiceRequest(PIN_PATH, "PUT", JsonUtil.toJson(accountLock)); } + /** Note: Setting a KBS Pin will clear this */ public void removePin() throws IOException { makeServiceRequest(PIN_PATH, "DELETE", null); } + public void setRegistrationLock(String registrationLock) throws IOException { + RegistrationLockV2 accountLock = new RegistrationLockV2(registrationLock); + makeServiceRequest(REGISTRATION_LOCK_PATH, "PUT", JsonUtil.toJson(accountLock)); + } + + public void removePinV2() throws IOException { + makeServiceRequest(REGISTRATION_LOCK_PATH, "DELETE", null); + } + public byte[] getSenderCertificateLegacy() throws IOException { String responseText = makeServiceRequest(SENDER_CERTIFICATE_LEGACY_PATH, "GET", null); return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); @@ -592,26 +615,27 @@ public class PushServiceSocket { } } - public String getContactDiscoveryAuthorization() throws IOException { - String response = makeServiceRequest(DIRECTORY_AUTH_PATH, "GET", null); - ContactDiscoveryCredentials token = JsonUtil.fromJson(response, ContactDiscoveryCredentials.class); - return Credentials.basic(token.getUsername(), token.getPassword()); + private String getCredentials(String authPath) throws IOException { + String response = makeServiceRequest(authPath, "GET", null, NO_HEADERS); + AuthCredentials token = JsonUtil.fromJson(response, AuthCredentials.class); + return token.asBasic(); } - public Pair> getContactDiscoveryRemoteAttestation(String authorization, RemoteAttestationRequest request, String mrenclave) - throws IOException - { - Response response = makeContactDiscoveryRequest(authorization, new LinkedList(), "/v1/attestation/" + mrenclave, "PUT", JsonUtil.toJson(request)); - ResponseBody body = response.body(); - List rawCookies = response.headers("Set-Cookie"); - List cookies = new LinkedList<>(); + public String getContactDiscoveryAuthorization() throws IOException { + return getCredentials(DIRECTORY_AUTH_PATH); + } - for (String cookie : rawCookies) { - cookies.add(cookie.split(";")[0]); - } + public String getKeyBackupServiceAuthorization() throws IOException { + return getCredentials(KBS_AUTH_PATH); + } + + public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName) + throws IOException + { + ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, null, "/v1/token/" + enclaveName, "GET", null).body(); if (body != null) { - return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies); + return JsonUtil.fromJson(body.string(), TokenResponse.class); } else { throw new NonSuccessfulResponseCodeException("Empty response!"); } @@ -620,7 +644,7 @@ public class PushServiceSocket { public DiscoveryResponse getContactDiscoveryRegisteredUsers(String authorizationToken, DiscoveryRequest request, List cookies, String mrenclave) throws IOException { - ResponseBody body = makeContactDiscoveryRequest(authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body(); + ResponseBody body = makeRequest(ClientSet.ContactDiscovery, authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body(); if (body != null) { return JsonUtil.fromJson(body.string(), DiscoveryResponse.class); @@ -629,6 +653,18 @@ public class PushServiceSocket { } } + public KeyBackupResponse putKbsData(String authorizationToken, KeyBackupRequest request, List cookies, String mrenclave) + throws IOException + { + ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, cookies, "/v1/backup/" + mrenclave, "PUT", JsonUtil.toJson(request)).body(); + + if (body != null) { + return JsonUtil.fromJson(body.string(), KeyBackupResponse.class); + } else { + throw new NonSuccessfulResponseCodeException("Empty response!"); + } + } + public void reportContactDiscoveryServiceMatch() throws IOException { makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "ok"), "PUT", ""); } @@ -922,7 +958,12 @@ public class PushServiceSocket { throw new PushNetworkException(e); } - throw new LockedException(accountLockFailure.length, accountLockFailure.timeRemaining); + AuthCredentials credentials = accountLockFailure.backupCredentials; + String basicStorageCredentials = credentials != null ? credentials.asBasic() : null; + + throw new LockedException(accountLockFailure.length, + accountLockFailure.timeRemaining, + basicStorageCredentials); } if (responseCode != 200 && responseCode != 204) { @@ -960,10 +1001,12 @@ public class PushServiceSocket { request.addHeader(header.getKey(), header.getValue()); } - if (unidentifiedAccess.isPresent()) { - request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey())); - } else if (credentialsProvider.getPassword() != null) { - request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider)); + if (!headers.containsKey("Authorization")) { + if (unidentifiedAccess.isPresent()) { + request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey())); + } else if (credentialsProvider.getPassword() != null) { + request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider)); + } } if (userAgent != null) { @@ -992,15 +1035,33 @@ public class PushServiceSocket { } } - private Response makeContactDiscoveryRequest(String authorization, List cookies, String path, String method, String body) + private ConnectionHolder[] clientsFor(ClientSet clientSet) { + switch (clientSet) { + case ContactDiscovery: + return contactDiscoveryClients; + case KeyBackup: + return keyBackupServiceClients; + default: + throw new AssertionError("Unknown attestation purpose"); + } + } + + Response makeRequest(ClientSet clientSet, String authorization, List cookies, String path, String method, String body) throws PushNetworkException, NonSuccessfulResponseCodeException { - ConnectionHolder connectionHolder = getRandom(contactDiscoveryClients, random); - OkHttpClient okHttpClient = connectionHolder.getClient() - .newBuilder() - .connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) - .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) - .build(); + ConnectionHolder connectionHolder = getRandom(clientsFor(clientSet), random); + + return makeRequest(connectionHolder, authorization, cookies, path, method, body); + } + + private Response makeRequest(ConnectionHolder connectionHolder, String authorization, List cookies, String path, String method, String body) + throws PushNetworkException, NonSuccessfulResponseCodeException + { + OkHttpClient okHttpClient = connectionHolder.getClient() + .newBuilder() + .connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) + .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) + .build(); Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path); @@ -1153,12 +1214,26 @@ public class PushServiceSocket { } } + private static class RegistrationLockV2 { + @JsonProperty + private String registrationLock; + + public RegistrationLockV2() {} + + public RegistrationLockV2(String registrationLock) { + this.registrationLock = registrationLock; + } + } + private static class RegistrationLockFailure { @JsonProperty private int length; @JsonProperty private long timeRemaining; + + @JsonProperty + private AuthCredentials backupCredentials; } private static class AttachmentDescriptor { @@ -1225,4 +1300,6 @@ public class PushServiceSocket { @Override public void handle(int responseCode) { } } + + public enum ClientSet { ContactDiscovery, KeyBackup } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteAttestationUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteAttestationUtil.java new file mode 100644 index 0000000000..4cc4cd30f4 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteAttestationUtil.java @@ -0,0 +1,79 @@ +package org.whispersystems.signalservice.internal.push; + +import org.whispersystems.curve25519.Curve25519; +import org.whispersystems.curve25519.Curve25519KeyPair; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.internal.contacts.crypto.Quote; +import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation; +import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationCipher; +import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; +import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest; +import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse; +import org.whispersystems.signalservice.internal.util.JsonUtil; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.SignatureException; +import java.util.LinkedList; +import java.util.List; + +import okhttp3.Response; +import okhttp3.ResponseBody; + +public final class RemoteAttestationUtil { + + private RemoteAttestationUtil() { + } + + public static RemoteAttestation getAndVerifyRemoteAttestation(PushServiceSocket socket, + PushServiceSocket.ClientSet clientSet, + KeyStore iasKeyStore, + String enclaveName, + String mrenclave, + String authorization) + throws IOException, Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException + { + Curve25519 curve = Curve25519.getInstance(Curve25519.BEST); + Curve25519KeyPair keyPair = curve.generateKeyPair(); + RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey()); + Pair> attestationResponsePair = getRemoteAttestation(socket, clientSet, authorization, attestationRequest, enclaveName); + RemoteAttestationResponse attestationResponse = attestationResponsePair.first(); + List attestationCookies = attestationResponsePair.second(); + + RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.getServerEphemeralPublic(), attestationResponse.getServerStaticPublic()); + Quote quote = new Quote(attestationResponse.getQuote()); + byte[] requestId = RemoteAttestationCipher.getRequestId(keys, attestationResponse); + + RemoteAttestationCipher.verifyServerQuote(quote, attestationResponse.getServerStaticPublic(), mrenclave); + + RemoteAttestationCipher.verifyIasSignature(iasKeyStore, attestationResponse.getCertificates(), attestationResponse.getSignatureBody(), attestationResponse.getSignature(), quote); + + return new RemoteAttestation(requestId, keys, attestationCookies); + } + + private static Pair> getRemoteAttestation(PushServiceSocket socket, + PushServiceSocket.ClientSet clientSet, + String authorization, + RemoteAttestationRequest request, + String enclaveName) + throws IOException + { + Response response = socket.makeRequest(clientSet, authorization, new LinkedList(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(request)); + ResponseBody body = response.body(); + List rawCookies = response.headers("Set-Cookie"); + List cookies = new LinkedList<>(); + + for (String cookie : rawCookies) { + cookies.add(cookie.split(";")[0]); + } + + if (body != null) { + return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies); + } else { + throw new NonSuccessfulResponseCodeException("Empty response!"); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java new file mode 100644 index 0000000000..b070fe1e74 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/InvalidPinException.java @@ -0,0 +1,8 @@ +package org.whispersystems.signalservice.internal.registrationpin; + +public final class InvalidPinException extends Exception { + + InvalidPinException(String message) { + super(message); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java new file mode 100644 index 0000000000..4384d1db4b --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinStretcher.java @@ -0,0 +1,166 @@ +package org.whispersystems.signalservice.internal.registrationpin; + +import org.whispersystems.signalservice.internal.util.Hex; +import org.whispersystems.signalservice.internal.util.Util; + +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.Mac; +import javax.crypto.SecretKeyFactory; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public final class PinStretcher { + + private static final String HMAC_SHA256 = "HmacSHA256"; + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + public static StretchedPin stretchPin(CharSequence pin) throws InvalidPinException { + return new StretchedPin(pin); + } + + public static class StretchedPin { + private final byte[] stretchedPin; + private final byte[] pinKey1; + private final byte[] kbsAccessKey; + + private StretchedPin(byte[] stretchedPin, byte[] pinKey1, byte[] kbsAccessKey) { + this.stretchedPin = stretchedPin; + this.pinKey1 = pinKey1; + this.kbsAccessKey = kbsAccessKey; + } + + private StretchedPin(CharSequence pin) throws InvalidPinException { + if (pin.length() < 4) throw new InvalidPinException("Pin too short"); + + char[] arabicPin = toArabic(pin); + + stretchedPin = pbkdf2HmacSHA256(arabicPin, "nosalt", 20000, 256); + + try { + Mac mac = Mac.getInstance(HMAC_SHA256); + mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256)); + mac.update("Master Key Encryption".getBytes(UTF_8)); + + pinKey1 = new byte[32]; + mac.doFinal(pinKey1, 0); + + mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256)); + mac.update("KBS Access Key".getBytes(UTF_8)); + + kbsAccessKey = new byte[32]; + mac.doFinal(kbsAccessKey, 0); + } catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) { + throw new AssertionError(e); + } + } + + public MasterKey withPinKey2(byte[] pinKey2) { + return new MasterKey(pinKey1, pinKey2, this); + } + + public MasterKey withNewSecurePinKey2() { + return withPinKey2(Util.getSecretBytes(32)); + } + + public byte[] getPinKey1() { + return pinKey1; + } + + public byte[] getStretchedPin() { + return stretchedPin; + } + + public byte[] getKbsAccessKey() { + return kbsAccessKey; + } + } + + public static class MasterKey extends StretchedPin { + private final byte[] pinKey2; + private final byte[] masterKey; + private final String registrationLock; + + private MasterKey(byte[] pinKey1, byte[] pinKey2, StretchedPin stretchedPin) { + super(stretchedPin.stretchedPin, stretchedPin.pinKey1, stretchedPin.kbsAccessKey); + + if (pinKey2.length != 32) { + throw new AssertionError("PinKey2 must be exactly 32 bytes"); + } + + this.pinKey2 = pinKey2.clone(); + + try { + Mac mac = Mac.getInstance(HMAC_SHA256); + + mac.init(new SecretKeySpec(pinKey1, HMAC_SHA256)); + mac.update(pinKey2); + + masterKey = new byte[32]; + mac.doFinal(masterKey, 0); + + mac.init(new SecretKeySpec(masterKey, HMAC_SHA256)); + mac.update("Registration Lock".getBytes(UTF_8)); + + byte[] registration_lock_token_bytes = new byte[32]; + mac.doFinal(registration_lock_token_bytes, 0); + registrationLock = Hex.toStringCondensed(registration_lock_token_bytes); + + } catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) { + throw new AssertionError(e); + } + } + + public byte[] getPinKey2() { + return pinKey2; + } + + public String getRegistrationLock() { + return registrationLock; + } + + public byte[] getMasterKey() { + return masterKey; + } + } + + private static byte[] pbkdf2HmacSHA256(char[] pin, String salt, int iterationCount, int outputSize) { + byte[] saltBytes = salt.getBytes(Charset.forName("UTF-8")); + + try { + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + PBEKeySpec spec = new PBEKeySpec(pin, saltBytes, iterationCount, outputSize); + byte[] encoded = skf.generateSecret(spec).getEncoded(); + + spec.clearPassword(); + + return encoded; + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new AssertionError("Could not stretch pin", e); + } + } + + /** + * Converts a string of not necessarily Arabic numerals to Arabic 0..9 characters. + */ + private static char[] toArabic(CharSequence numerals) throws InvalidPinException { + int length = numerals.length(); + char[] arabic = new char[length]; + + for (int i = 0; i < length; i++) { + int digit = Character.digit(numerals.charAt(i), 10); + + if (digit < 0) { + throw new InvalidPinException("Pin must only consist of decimals"); + } + + arabic[i] = (char) ('0' + digit); + } + + return arabic; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/Util.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/Util.java index 3de6a2442b..b1a3f5b4af 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/Util.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/Util.java @@ -10,7 +10,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Collection; @@ -71,13 +70,9 @@ public class Util { } public static byte[] getSecretBytes(int size) { - try { - byte[] secret = new byte[size]; - SecureRandom.getInstance("SHA1PRNG").nextBytes(secret); - return secret; - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } + byte[] secret = new byte[size]; + new SecureRandom().nextBytes(secret); + return secret; } public static byte[] getRandomLengthBytes(int maxSize) { diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/SigningCertificateTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/SigningCertificateTest.java index 9c2d69c63f..d5efbcebbd 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/SigningCertificateTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/SigningCertificateTest.java @@ -7,6 +7,7 @@ import org.whispersystems.signalservice.internal.contacts.crypto.SigningCertific import org.whispersystems.util.Base64; import java.io.IOException; +import java.net.URLDecoder; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -22,7 +23,7 @@ public class SigningCertificateTest extends TestCase { } public void testGoodSignature() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, CertPathValidatorException, SignatureException { - String certificateChain = "-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A"; + String certificateChain = URLDecoder.decode("-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A"); String signature = "Kn2Ya2T039qvEWIzIQeSksNyyCQIkcVjciClcp3a6C766dJANXxLLIn6CfyvUZddMtePrTOLpC2e5QTQxB4RwtWmFfr7nxRdFUtA3dH2DAQL5DqqlmPv46ZWSPfiiOXUsu8vNgX3Z4Znt4Q+dIPIquNPY8ZmiAcpKR7n2K3QtabgOnJ2EyngabY3LMQTtriXbZjpl53ynhVhV1rciMdvMaTz4DUYt7gKi+KeNd3CBFSev+eTgYPC3em96J/3bfVR+wC5m3JGbIBCrwAsbO05JkiNIMck3s+p4d/hwiABR75EplxaWmGgIm6VvUKtGhdJ/cNrmF0nxMX6Vi6N2WaLTA=="; String signatureBody = "{\"id\":\"287419896494669543891634765983074535548\",\"timestamp\":\"2019-03-11T20:01:21.658293\",\"version\":3,\"isvEnclaveQuoteStatus\":\"OK\",\"isvEnclaveQuoteBody\":\"AgAAADILAAAIAAcAAAAAAPiLWcRSSA3shraxepsGV9qF4zYUPJgE42ZZZXS2G9zaBQUCBP//AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAHAAAAAAAAAM1s/DQpN7I7G907v5chqlYVrJ/1CnXFUn1EHNMnaCbJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrzm117Qj8NlEllyDkV4Pae4UgsPjgVXtAA5UsG90gVgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHgz6GaO6bkxfPLBYcR5rEf9Itrt81OEanXteSMcd/BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}"; @@ -35,7 +36,7 @@ public class SigningCertificateTest extends TestCase { } public void testBadSignature() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, CertPathValidatorException, SignatureException { - String certificateChain = "-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A"; + String certificateChain = URLDecoder.decode("-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A"); String signature = "Kn2Ya2T039qvEWIzIQeSksNyyCQIkcVjciClcp3a6C766dJANXxLLIn6CfyvUZddMtePrTOLpC2e5QTQxB4RwtWmFfr7nxRdFUtA3dH2DAQL5DqqlmPv46ZWSPfiiOXUsu8vNgX3Z4Znt4Q+dIPIquNPY8ZmiAcpKR7n2K3QtabgOnJ2EyngabY3LMQTtriXbZjpl53ynhVhV1rciMdvMaTz4DUYt7gKi+KeNd3CBFSev+eTgYPC3em96J/3bfVR+wC5m3JGbIBCrwAsbO05JkiNIMck3s+p4d/hwiABR75EplxaWmGgIm6VvUKtGhdJ/cNrmF0nxMX6Vi6N2WaLTA=="; String signatureBody = "{\"id\":\"287419896494669543891634765983074535548\",\"timestamp\":\"2019-03-11T20:01:21.658293\",\"version\":3,\"isvEnclaveQuoteStatus\":\"OK\",\"isvEnclaveQuoteBody\":\"AgAAADILAAAIAAcAAAAAAPiLWcRSSA3shraxepsGV9qF4zYUPJgE42ZZZXS2G9zaBQUCBP//AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAHAAAAAAAAAM1s/DQpN7I7G907v5chqlYVrJ/1CnXFUn1EHNMnaCbJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADrzm117Qj8NlEllyDkV4Pae4UgsPjgVXtAA5UsG90gVgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHgz6GaO6bkxfPLBYcR5rEf9Itrt81OEanXteSMcd/BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}"; @@ -63,7 +64,7 @@ public class SigningCertificateTest extends TestCase { } public void testBadChain() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, CertPathValidatorException, SignatureException { - String certificateChain = "-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgAA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A"; + String certificateChain = URLDecoder.decode("-----BEGIN%20CERTIFICATE-----%0AMIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIw%0AMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1Nh%0AbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwk%0ASW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG%0A9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA%2Bt%0AbeCTUR106AL1ENcWA4FX3K%2BE9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtId%0Acv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuv%0ALUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV%2BW9tOhA%0AImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt%2B%2BqO/6%2BKAXJuKwZqjRlEtSEz8%0AgZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGh%0AMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN%2Bs1fDuHAVE8MA4GA1UdDwEB/wQEAwIG%0AwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVk%0Ac2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJl%0AcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4r%0ARq%2BZKE%2B7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9%0AlpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYv%0AWLrtXXfFBSSPD4Afn7%2B3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUd%0AZseZCcaZZZn65tdqee8UXZlDvx0%2BNdO0LR%2B5pFy%2BjuM0wWbu59MvzcmTXbjsi7HY%0A6zd53Yq5K244fwFHRQ8eOB0IWB%2B4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW7%0A2uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN%2BKwPbpA39%2BxOsStjhP9N1Y1a2%0AtQAVo%2ByVgLgV2Hws73Fc0o3wC78qPEA%2Bv2aRs/Be3ZFDgDyghc/1fgU%2B7C%2BP6kbq%0Ad4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgAA%3D%3D%0A-----END%20CERTIFICATE-----%0A-----BEGIN%20CERTIFICATE-----%0AMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV%0ABAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV%0ABAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0%0AYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy%0AMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL%0AU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD%0ADCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G%0ACSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR%2BtXc8u1EtJzLA10Feu1Wg%2Bp7e%0ALmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh%0ArgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT%0AL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe%0ANpEJUmg4ktal4qgIAxk%2BQHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ%0AbyinkNndn%2BBgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H%0AafuVeLHcDsRp6hol4P%2BZFIhu8mmbI1u0hH3W/0C2BuYXB5PC%2B5izFFh/nP0lc2Lf%0A6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM%0ARoOaX4AS%2B909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX%0AMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50%0AL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW%0ABBR4Q3t2pn680K9%2BQjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9%2BQjfr%0ANXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq%0AhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir%0AIEqucRiJSSx%2BHjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi%2BripMtPZ%0AsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi%0AzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra%0AUd4APK0wZTGtfPXU7w%2BIBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA%0A152Sq049ESDz%2B1rRGc2NVEqh1KaGXmtXvqxXcTB%2BLjy5Bw2ke0v8iGngFBPqCTVB%0A3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5%2BxmBc388v9Dm21HGfcC8O%0ADD%2BgT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R%2BmJTLwPXVMrv%0ADaVzWh5aiEx%2BidkSGMnX%0A-----END%20CERTIFICATE-----%0A"); KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(getClass().getResourceAsStream("/ias.jks"), "whisper".toCharArray()); diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java new file mode 100644 index 0000000000..d0d7bae50b --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchFailureTest.java @@ -0,0 +1,31 @@ +package org.whispersystems.signalservice.internal.registrationpin; + +import org.junit.Test; + +public final class PinStretchFailureTest { + + @Test(expected = InvalidPinException.class) + public void non_numeric_pin() throws InvalidPinException { + PinStretcher.stretchPin("A"); + } + + @Test(expected = InvalidPinException.class) + public void empty() throws InvalidPinException { + PinStretcher.stretchPin(""); + } + + @Test(expected = InvalidPinException.class) + public void too_few_digits() throws InvalidPinException { + PinStretcher.stretchPin("123"); + } + + @Test(expected = AssertionError.class) + public void pin_key_2_too_short() throws InvalidPinException { + PinStretcher.stretchPin("0000").withPinKey2(new byte[31]); + } + + @Test(expected = AssertionError.class) + public void pin_key_2_too_long() throws InvalidPinException { + PinStretcher.stretchPin("0000").withPinKey2(new byte[33]); + } +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java new file mode 100644 index 0000000000..4624e1766e --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/registrationpin/PinStretchTest.java @@ -0,0 +1,127 @@ +package org.whispersystems.signalservice.internal.registrationpin; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.whispersystems.signalservice.internal.util.Hex; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public final class PinStretchTest { + + private final String pin; + private final byte[] expectedStretchedPin; + private final byte[] expectedKeyPin1; + private final byte[] pinKey2; + private final byte[] expectedMasterKey; + private final String expectedRegistrationLock; + private final byte[] expectedKbsAccessKey; + + @Parameterized.Parameters + public static Collection 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()); + } +} diff --git a/res/values/strings.xml b/res/values/strings.xml index f2e71f12d7..22fdba048a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -687,6 +687,8 @@ Failed to verify the CAPTCHA Next Continue + Continue (%d attempts left) + Continue (last attempt!) Take privacy with you.\nBe yourself in every message. Enter your phone number to get started You will receive a verification code. Carrier rates may apply. @@ -1695,6 +1697,8 @@ Error connecting to the service Disable Registration Lock PIN? Disable + PIN Incorrect + You have %d tries remaining Backups Signal is locked TAP TO UNLOCK diff --git a/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 02070fa460..5e79330919 100644 --- a/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -4,13 +4,17 @@ import android.app.Application; import androidx.annotation.NonNull; +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.IncomingMessageProcessor; import org.thoughtcrime.securesms.gcm.MessageRetriever; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.service.IncomingMessageObserver; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.IasKeyStore; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.api.KeyBackupService; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; @@ -60,6 +64,14 @@ public class ApplicationDependencies { return accountManager; } + public static synchronized @NonNull KeyBackupService getKeyBackupService() { + if (!FeatureFlags.KBS) throw new AssertionError(); + return getSignalServiceAccountManager().getKeyBackupService(IasKeyStore.getIasKeyStore(application), + BuildConfig.KEY_BACKUP_ENCLAVE_NAME, + BuildConfig.KEY_BACKUP_MRENCLAVE, + 10); + } + public static synchronized @NonNull SignalServiceMessageSender getSignalServiceMessageSender() { assertInitialization(); diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 8902eb5fd1..11b0508ec1 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.jobs; import android.app.Application; + import androidx.annotation.NonNull; import org.thoughtcrime.securesms.jobmanager.Constraint; @@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.migrations.DatabaseMigrationJob; import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; import org.thoughtcrime.securesms.migrations.MigrationCompleteJob; import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob; +import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob; import org.thoughtcrime.securesms.migrations.UuidMigrationJob; import java.util.Arrays; @@ -93,6 +95,7 @@ public final class JobManagerFactories { put(RecipientSearchMigrationJob.KEY, new RecipientSearchMigrationJob.Factory()); put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory()); put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory()); + put(RegistrationPinV2MigrationJob.KEY, new RegistrationPinV2MigrationJob.Factory()); // Dead jobs put("PushContentReceiveJob", new FailingJob.Factory()); diff --git a/src/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/src/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index f4bb2b5024..cc9fe9d762 100644 --- a/src/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -2,14 +2,12 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; - -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; @@ -47,12 +45,23 @@ public class RefreshAttributesJob extends BaseJob { public void onRun() throws IOException { int registrationId = TextSecurePreferences.getLocalRegistrationId(context); boolean fetchesMessages = TextSecurePreferences.isFcmDisabled(context); - String pin = TextSecurePreferences.getRegistrationLockPin(context); byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(context); boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context); + String pin = null; + String registrationLockToken = null; + + if (TextSecurePreferences.isRegistrationLockEnabled(context)) { + if (TextSecurePreferences.hasOldRegistrationLockPin(context)) { + //noinspection deprecation Ok to read here as they have not migrated + pin = TextSecurePreferences.getDeprecatedRegistrationLockPin(context); + } else { + registrationLockToken = TextSecurePreferences.getRegistrationLockToken(context); + } + } SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); - signalAccountManager.setAccountAttributes(null, registrationId, fetchesMessages, pin, + signalAccountManager.setAccountAttributes(null, registrationId, fetchesMessages, + pin, registrationLockToken, unidentifiedAccessKey, universalUnidentifiedAccess); } diff --git a/src/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java b/src/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java index a2d5df9e48..b6001390ce 100644 --- a/src/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java +++ b/src/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java @@ -6,18 +6,16 @@ import android.content.Context; import android.graphics.Typeface; import android.os.AsyncTask; import android.os.Build; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import android.text.Editable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextUtils; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.StyleSpan; import android.util.DisplayMetrics; -import org.thoughtcrime.securesms.logging.Log; import android.view.Display; import android.view.View; import android.view.ViewGroup; @@ -28,23 +26,43 @@ import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThemeUtil; -import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.RegistrationLockData; import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException; +import org.whispersystems.signalservice.internal.registrationpin.PinStretcher; import java.io.IOException; -public class RegistrationLockDialog { +public final class RegistrationLockDialog { - private static final String TAG = RegistrationLockDialog.class.getSimpleName(); + private static final String TAG = Log.tag(RegistrationLockDialog.class); public static void showReminderIfNecessary(@NonNull Context context) { - if (!RegistrationLockReminders.needsReminder(context)) return; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return; + if (!RegistrationLockReminders.needsReminder(context)) return; + + if (!TextSecurePreferences.hasOldRegistrationLockPin(context) && + TextUtils.isEmpty(TextSecurePreferences.getRegistrationLockToken(context))) { + // Neither v1 or v2 to check against + Log.w(TAG, "Reg lock enabled, but no pin stored to verify against"); + return; + } AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark : R.style.RationaleDialogLight) .setView(R.layout.registration_lock_reminder_view) @@ -61,15 +79,15 @@ public class RegistrationLockDialog { dialog.show(); dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT); - EditText pinEditText = dialog.findViewById(R.id.pin); - TextView reminder = dialog.findViewById(R.id.reminder); + EditText pinEditText = dialog.findViewById(R.id.pin); + TextView reminder = dialog.findViewById(R.id.reminder); - assert pinEditText != null; - assert reminder != null; + if (pinEditText == null) throw new AssertionError(); + if (reminder == null) throw new AssertionError(); SpannableString reminderIntro = new SpannableString(context.getString(R.string.RegistrationLockDialog_reminder)); - SpannableString reminderText = new SpannableString(context.getString(R.string.RegistrationLockDialog_registration_lock_is_enabled_for_your_phone_number)); - SpannableString forgotText = new SpannableString(context.getString(R.string.RegistrationLockDialog_i_forgot_my_pin)); + SpannableString reminderText = new SpannableString(context.getString(R.string.RegistrationLockDialog_registration_lock_is_enabled_for_your_phone_number)); + SpannableString forgotText = new SpannableString(context.getString(R.string.RegistrationLockDialog_i_forgot_my_pin)); ClickableSpan clickableSpan = new ClickableSpan() { @Override @@ -83,14 +101,23 @@ public class RegistrationLockDialog { } }; - reminderIntro.setSpan(new StyleSpan(Typeface.BOLD), 0, reminderIntro.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); forgotText.setSpan(clickableSpan, 0, forgotText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); reminder.setText(new SpannableStringBuilder(reminderIntro).append(" ").append(reminderText).append(" ").append(forgotText)); reminder.setMovementMethod(LinkMovementMethod.getInstance()); - pinEditText.addTextChangedListener(new TextWatcher() { + pinEditText.addTextChangedListener(TextSecurePreferences.hasOldRegistrationLockPin(context) + ? getV1PinWatcher(context, dialog) + : getV2PinWatcher(context, dialog)); + } + + private static TextWatcher getV1PinWatcher(@NonNull Context context, AlertDialog dialog) { + //noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system. + String pin = TextSecurePreferences.getDeprecatedRegistrationLockPin(context); + + return new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override @@ -98,17 +125,56 @@ public class RegistrationLockDialog { @Override public void afterTextChanged(Editable s) { - if (s != null && s.toString().replace(" ", "").equals(TextSecurePreferences.getRegistrationLockPin(context))) { + if (s != null && s.toString().replace(" ", "").equals(pin)) { dialog.dismiss(); RegistrationLockReminders.scheduleReminder(context, true); + + if (FeatureFlags.KBS) { + Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2"); + ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob()); + } } } - }); + }; + } + private static TextWatcher getV2PinWatcher(@NonNull Context context, AlertDialog dialog) { + String registrationLockToken = TextSecurePreferences.getRegistrationLockToken(context); + byte[] pinKey2 = TextSecurePreferences.getRegistrationLockPinKey2(context); + TokenResponse registrationLockTokenResponse = TextSecurePreferences.getRegistrationLockTokenResponse(context); + + if (registrationLockToken == null) throw new AssertionError("No V2 reg lock token set at time of reminder"); + if (pinKey2 == null) throw new AssertionError("No pin key2 set at time of reminder"); + if (registrationLockTokenResponse == null) throw new AssertionError("No registrationLockTokenResponse set at time of reminder"); + + return new TextWatcher() { + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if (s == null) return; + String pin = s.toString(); + if (TextUtils.isEmpty(pin)) return; + if (pin.length() < 4) return; + + try { + if (registrationLockToken.equals(PinStretcher.stretchPin(pin).withPinKey2(pinKey2).getRegistrationLock())) { + dialog.dismiss(); + RegistrationLockReminders.scheduleReminder(context, true); + } + } catch (InvalidPinException e) { + Log.w(TAG, e); + } + } + }; } @SuppressLint("StaticFieldLeak") - public static void showRegistrationLockPrompt(@NonNull Context context, @NonNull SwitchPreferenceCompat preference, @NonNull SignalServiceAccountManager accountManager) { + public static void showRegistrationLockPrompt(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) { AlertDialog dialog = new AlertDialog.Builder(context) .setTitle(R.string.RegistrationLockDialog_registration_lock) .setView(R.layout.registration_lock_dialog_view) @@ -123,9 +189,9 @@ public class RegistrationLockDialog { EditText repeat = dialog.findViewById(R.id.repeat); ProgressBar progressBar = dialog.findViewById(R.id.progress); - assert pin != null; - assert repeat != null; - assert progressBar != null; + if (pin == null) throw new AssertionError(); + if (repeat == null) throw new AssertionError(); + if (progressBar == null) throw new AssertionError(); String pinValue = pin.getText().toString().replace(" ", ""); String repeatValue = repeat.getText().toString().replace(" ", ""); @@ -151,12 +217,33 @@ public class RegistrationLockDialog { @Override protected Boolean doInBackground(Void... voids) { try { - accountManager.setPin(Optional.of(pinValue)); - TextSecurePreferences.setRegistrationLockPin(context, pinValue); - TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); - TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL); + if (!FeatureFlags.KBS) { + Log.i(TAG, "Setting V1 pin"); + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + accountManager.setPin(pinValue); + TextSecurePreferences.setDeprecatedRegistrationLockPin(context, pinValue); + } else { + Log.i(TAG, "Setting pin on KBS"); + KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); + RegistrationLockData kbsData = keyBackupService.newPinChangeSession() + .setPin(pinValue); + RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse()) + .restorePin(pinValue); + String restoredLock = restoredData.getMasterKey() + .getRegistrationLock(); + + if (!restoredLock.equals(kbsData.getMasterKey().getRegistrationLock())) { + throw new AssertionError("Failed to set the pin correctly"); + } else { + Log.i(TAG, "Set and retrieved pin on KBS successfully"); + } + + TextSecurePreferences.setRegistrationLockMasterKey(context, restoredData, System.currentTimeMillis()); + TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); + TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL); + } return true; - } catch (IOException e) { + } catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException | InvalidPinException e) { Log.w(TAG, e); return false; } @@ -182,7 +269,8 @@ public class RegistrationLockDialog { } @SuppressLint("StaticFieldLeak") - public static void showRegistrationUnlockPrompt(@NonNull Context context, @NonNull SwitchPreferenceCompat preference, @NonNull SignalServiceAccountManager accountManager) { + public static void showRegistrationUnlockPrompt(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) { + AlertDialog dialog = new AlertDialog.Builder(context) .setTitle(R.string.RegistrationLockDialog_disable_registration_lock_pin) .setView(R.layout.registration_unlock_dialog_view) @@ -207,9 +295,26 @@ public class RegistrationLockDialog { @Override protected Boolean doInBackground(Void... voids) { try { - accountManager.setPin(Optional.absent()); + if (!FeatureFlags.KBS) { + Log.i(TAG, "Removing v1 registration lock pin from server"); + ApplicationDependencies.getSignalServiceAccountManager().removeV1Pin(); + } else { + Log.i(TAG, "Removing v2 registration lock pin from server"); + TokenResponse currentToken = TextSecurePreferences.getRegistrationLockTokenResponse(context); + + KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); + keyBackupService.newPinChangeSession(currentToken).removePin(); + TextSecurePreferences.setRegistrationLockMasterKey(context, null, System.currentTimeMillis()); + + // It is possible a migration has not occurred, in this case, we need to remove the old V1 Pin + if (TextSecurePreferences.hasOldRegistrationLockPin(context)) { + Log.i(TAG, "Removing v1 registration lock pin from server"); + ApplicationDependencies.getSignalServiceAccountManager().removeV1Pin(); + TextSecurePreferences.clearOldRegistrationLockPin(context); + } + } return true; - } catch (IOException e) { + } catch (IOException | UnauthenticatedResponseException e) { Log.w(TAG, e); return false; } diff --git a/src/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java b/src/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java index d000870cdb..95af0316c3 100644 --- a/src/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java +++ b/src/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.lock; import android.content.Context; + import androidx.annotation.NonNull; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -23,7 +24,7 @@ public class RegistrationLockReminders { public static final long INITIAL_INTERVAL = INTERVALS.first(); public static boolean needsReminder(@NonNull Context context) { - if (!TextSecurePreferences.isRegistrationtLockEnabled(context)) return false; + if (!TextSecurePreferences.isRegistrationLockEnabled(context)) return false; long lastReminderTime = TextSecurePreferences.getRegistrationLockLastReminderTime(context); long nextIntervalTime = TextSecurePreferences.getRegistrationLockNextReminderInterval(context); @@ -47,5 +48,4 @@ public class RegistrationLockReminders { TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval); } - } diff --git a/src/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java b/src/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java new file mode 100644 index 0000000000..63842de3cb --- /dev/null +++ b/src/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.migrations; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.jobs.BaseJob; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.api.RegistrationLockData; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException; + +import java.io.IOException; + +/** + * Deliberately not a {@link MigrationJob} because it is not something that needs to run at app start. + * This migration can run at anytime. + */ +public final class RegistrationPinV2MigrationJob extends BaseJob { + + private static final String TAG = Log.tag(RegistrationPinV2MigrationJob.class); + + public static final String KEY = "RegistrationPinV2MigrationJob"; + + public RegistrationPinV2MigrationJob() { + this(new Parameters.Builder() + .setQueue(KEY) + .setMaxInstances(1) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(Job.Parameters.IMMORTAL) + .setMaxAttempts(Job.Parameters.UNLIMITED) + .build()); + } + + private RegistrationPinV2MigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public @NonNull Data serialize() { + return Data.EMPTY; + } + + @Override + protected void onRun() throws IOException, UnauthenticatedResponseException { + if (!FeatureFlags.KBS) { + Log.i(TAG, "Not migrating pin to KBS"); + return; + } + + if (!TextSecurePreferences.isRegistrationLockEnabled(context)) { + Log.i(TAG, "Registration lock disabled"); + return; + } + + if (!TextSecurePreferences.hasOldRegistrationLockPin(context)) { + Log.i(TAG, "No old pin to migrate"); + return; + } + + //noinspection deprecation Only acceptable place to read the old pin. + String registrationLockPin = TextSecurePreferences.getDeprecatedRegistrationLockPin(context); + + if (registrationLockPin == null | TextUtils.isEmpty(registrationLockPin)) { + Log.i(TAG, "No old pin to migrate"); + return; + } + + Log.i(TAG, "Migrating pin to Key Backup Service"); + + try { + RegistrationLockData registrationPinV2Key = ApplicationDependencies.getKeyBackupService() + .newPinChangeSession() + .setPin(registrationLockPin); + + TextSecurePreferences.setRegistrationLockMasterKey(context, registrationPinV2Key, System.currentTimeMillis()); + } catch (InvalidPinException e) { + Log.w(TAG, "The V1 pin cannot be migrated.", e); + return; + } + + Log.i(TAG, "Pin migrated to Key Backup Service"); + } + + @Override + protected boolean onShouldRetry(@NonNull Exception e) { + return e instanceof IOException; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onCanceled() { + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull RegistrationPinV2MigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new RegistrationPinV2MigrationJob(parameters); + } + } +} diff --git a/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index 9839372ddb..4f062f15ad 100644 --- a/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -4,11 +4,12 @@ import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.widget.Toast; + import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.preference.CheckBoxPreference; import androidx.preference.Preference; -import android.widget.Toast; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; @@ -24,7 +25,6 @@ import org.thoughtcrime.securesms.lock.RegistrationLockDialog; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -148,12 +148,10 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment private class AccountLockClickListener implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(Preference preference) { - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - if (((SwitchPreferenceCompat)preference).isChecked()) { - RegistrationLockDialog.showRegistrationUnlockPrompt(getContext(), (SwitchPreferenceCompat)preference, accountManager); + RegistrationLockDialog.showRegistrationUnlockPrompt(requireContext(), (SwitchPreferenceCompat)preference); } else { - RegistrationLockDialog.showRegistrationLockPrompt(getContext(), (SwitchPreferenceCompat)preference, accountManager); + RegistrationLockDialog.showRegistrationLockPrompt(requireContext(), (SwitchPreferenceCompat)preference); } return true; @@ -218,13 +216,13 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment final String offRes = context.getString(R.string.ApplicationPreferencesActivity_off); if (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context)) { - if (TextSecurePreferences.isRegistrationtLockEnabled(context)) { + if (TextSecurePreferences.isRegistrationLockEnabled(context)) { return context.getString(privacySummaryResId, offRes, onRes); } else { return context.getString(privacySummaryResId, offRes, offRes); } } else { - if (TextSecurePreferences.isRegistrationtLockEnabled(context)) { + if (TextSecurePreferences.isRegistrationLockEnabled(context)) { return context.getString(privacySummaryResId, onRes, onRes); } else { return context.getString(privacySummaryResId, onRes, offRes); diff --git a/src/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/src/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java index 9008ee98a6..b58fab0ce6 100644 --- a/src/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java +++ b/src/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.push; import android.content.Context; + import androidx.annotation.Nullable; import org.thoughtcrime.securesms.BuildConfig; @@ -9,6 +10,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; +import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; @@ -116,29 +118,36 @@ public class SignalServiceNetworkAccess { final SignalContactDiscoveryUrl omanGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com.om/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); final SignalContactDiscoveryUrl qatarGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com.qa/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); + final SignalKeyBackupServiceUrl signalContactDiscoveryUrl = new SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); + this.censorshipConfiguration = new HashMap() {{ put(COUNTRY_CODE_EGYPT, new SignalServiceConfiguration(new SignalServiceUrl[] {egyptGoogleService, baseGoogleService, baseAndroidService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, new SignalCdnUrl[] {egyptGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn, mailAndroidCdn}, - new SignalContactDiscoveryUrl[] {egyptGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery})); + new SignalContactDiscoveryUrl[] {egyptGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, + new SignalKeyBackupServiceUrl[] { signalContactDiscoveryUrl })); put(COUNTRY_CODE_UAE, new SignalServiceConfiguration(new SignalServiceUrl[] {uaeGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, new SignalCdnUrl[] {uaeGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, - new SignalContactDiscoveryUrl[] {uaeGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery})); + new SignalContactDiscoveryUrl[] {uaeGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, + new SignalKeyBackupServiceUrl[] { signalContactDiscoveryUrl })); put(COUNTRY_CODE_OMAN, new SignalServiceConfiguration(new SignalServiceUrl[] {omanGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, new SignalCdnUrl[] {omanGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, - new SignalContactDiscoveryUrl[] {omanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery})); + new SignalContactDiscoveryUrl[] {omanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, + new SignalKeyBackupServiceUrl[] { signalContactDiscoveryUrl })); put(COUNTRY_CODE_QATAR, new SignalServiceConfiguration(new SignalServiceUrl[] {qatarGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, new SignalCdnUrl[] {qatarGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, - new SignalContactDiscoveryUrl[] {qatarGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery})); + new SignalContactDiscoveryUrl[] {qatarGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, + new SignalKeyBackupServiceUrl[] { signalContactDiscoveryUrl })); }}; this.uncensoredConfiguration = new SignalServiceConfiguration(new SignalServiceUrl[] {new SignalServiceUrl(BuildConfig.SIGNAL_URL, new SignalServiceTrustStore(context))}, new SignalCdnUrl[] {new SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, new SignalServiceTrustStore(context))}, - new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))}); + new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))}, + new SignalKeyBackupServiceUrl[] { new SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, new SignalServiceTrustStore(context)) }); this.censoredCountries = this.censorshipConfiguration.keySet().toArray(new String[0]); } diff --git a/src/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java b/src/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java index 2f477d9154..473c893f77 100644 --- a/src/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java +++ b/src/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java @@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest; import org.thoughtcrime.securesms.registration.service.RegistrationService; import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; import java.util.ArrayList; import java.util.Collections; @@ -106,7 +107,7 @@ public final class EnterCodeFragment extends BaseRegistrationFragment { RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret()); - registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, + registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, null, null, new CodeVerificationRequest.VerifyCallback() { @Override @@ -120,7 +121,8 @@ public final class EnterCodeFragment extends BaseRegistrationFragment { } @Override - public void onIncorrectRegistrationLockPin(long timeRemaining) { + public void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials) { + model.setStorageCredentials(storageCredentials); keyboard.displayLocked().addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean r) { @@ -130,6 +132,12 @@ public final class EnterCodeFragment extends BaseRegistrationFragment { }); } + @Override + public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse triesRemaining) { + // Unexpected, because at this point, no pin has been provided by the user. + throw new AssertionError(); + } + @Override public void onTooManyAttempts() { keyboard.displayFailure().addListener(new AssertedSuccessListener() { diff --git a/src/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java b/src/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java index 4a7f543b70..fffeb5428e 100644 --- a/src/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java +++ b/src/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java @@ -19,16 +19,23 @@ import androidx.navigation.Navigation; import com.dd.CircularProgressButton; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest; import org.thoughtcrime.securesms.registration.service.RegistrationService; import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; +import org.thoughtcrime.securesms.util.concurrent.SimpleTask; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import java.io.IOException; import java.util.concurrent.TimeUnit; public final class RegistrationLockFragment extends BaseRegistrationFragment { + private static final String TAG = Log.tag(RegistrationLockFragment.class); + private EditText pinEntry; private CircularProgressButton pinButton; + private long timeRemaining; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -51,7 +58,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { String code = getModel().getTextCodeEntered(); - long timeRemaining = RegistrationLockFragmentArgs.fromBundle(requireArguments()).getTimeRemaining(); + timeRemaining = RegistrationLockFragmentArgs.fromBundle(requireArguments()).getTimeRemaining(); pinForgotButton.setOnClickListener(v -> handleForgottenPin(timeRemaining)); @@ -87,6 +94,36 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { hideKeyboard(requireContext(), pinEntry); handlePinEntry(); }); + + RegistrationViewModel model = getModel(); + model.getTokenResponseCredentialsPair() + .observe(this, pair -> { + TokenResponse token = pair.first(); + String credentials = pair.second(); + updateContinueText(token, credentials); + }); + + model.onRegistrationLockFragmentCreate(); + } + + private void updateContinueText(@Nullable TokenResponse tokenResponse, @Nullable String storageCredentials) { + if (tokenResponse == null) { + if (storageCredentials == null) { + pinButton.setIdleText(getString(R.string.RegistrationActivity_continue)); + } else { + // TODO: This is the case where we can determine they are locked out + // no token, but do have storage credentials. Might want to change text. + pinButton.setIdleText(getString(R.string.RegistrationActivity_continue)); + } + } else { + int triesRemaining = tokenResponse.getTries(); + if (triesRemaining == 1) { + pinButton.setIdleText(getString(R.string.RegistrationActivity_continue_last_attempt)); + } else { + pinButton.setIdleText(getString(R.string.RegistrationActivity_continue_d_attempts_left, triesRemaining)); + } + } + pinButton.setText(pinButton.getIdleText()); } private void handlePinEntry() { @@ -99,10 +136,17 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { RegistrationViewModel model = getModel(); RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret()); + String storageCredentials = model.getBasicStorageCredentials(); + TokenResponse tokenResponse = model.getKeyBackupCurrentToken(); setSpinning(pinButton); - registrationService.verifyAccount(requireActivity(), model.getFcmToken(), model.getTextCodeEntered(), pin, new CodeVerificationRequest.VerifyCallback() { + registrationService.verifyAccount(requireActivity(), + model.getFcmToken(), + model.getTextCodeEntered(), + pin, storageCredentials, tokenResponse, + + new CodeVerificationRequest.VerifyCallback() { @Override public void onSuccessfulRegistration() { @@ -112,13 +156,34 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { } @Override - public void onIncorrectRegistrationLockPin(long timeRemaining) { + public void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials) { + model.setStorageCredentials(storageCredentials); cancelSpinning(pinButton); pinEntry.setText(""); Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_registration_lock_pin, Toast.LENGTH_LONG).show(); } + @Override + public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse tokenResponse) { + cancelSpinning(pinButton); + + model.setKeyBackupCurrentToken(tokenResponse); + + int triesRemaining = tokenResponse.getTries(); + + if (triesRemaining == 0) { + handleForgottenPin(timeRemaining); + return; + } + + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.RegistrationActivity_pin_incorrect) + .setMessage(getString(R.string.RegistrationActivity_you_have_d_tries_remaining, triesRemaining)) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + @Override public void onTooManyAttempts() { cancelSpinning(pinButton); diff --git a/src/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/src/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java index e25f155de8..728e23cffb 100644 --- a/src/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java +++ b/src/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java @@ -6,7 +6,6 @@ import android.os.AsyncTask; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.crypto.SessionUtil; @@ -20,20 +19,28 @@ import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; import org.thoughtcrime.securesms.jobs.RotateCertificateJob; import org.thoughtcrime.securesms.lock.RegistrationLockReminders; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob; import org.thoughtcrime.securesms.push.AccountManagerFactory; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.RegistrationLockData; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.exceptions.RateLimitException; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; import org.whispersystems.signalservice.internal.push.LockedException; +import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException; import java.io.IOException; import java.util.List; @@ -43,9 +50,16 @@ public final class CodeVerificationRequest { private static final String TAG = Log.tag(CodeVerificationRequest.class); + static TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException { + if (basicStorageCredentials == null) return null; + if (!FeatureFlags.KBS) return null; + return ApplicationDependencies.getKeyBackupService().getToken(basicStorageCredentials); + } + private enum Result { SUCCESS, PIN_LOCKED, + KBS_WRONG_PIN, RATE_LIMITED, ERROR } @@ -53,30 +67,35 @@ public final class CodeVerificationRequest { /** * Asynchronously verify the account via the code. * - * @param fcmToken The FCM token for the device. - * @param code The code that was delivered to the user. - * @param pin The users registration pin. - * @param callback Exactly one method on this callback will be called. + * @param fcmToken The FCM token for the device. + * @param code The code that was delivered to the user. + * @param pin The users registration pin. + * @param callback Exactly one method on this callback will be called. + * @param kbsTokenResponse By keeping the token, on failure, a newly returned token will be reused in subsequent pin + * attempts, preventing certain attacks, we can also track the attempts making missing replies easier to spot. */ static void verifyAccount(@NonNull Context context, @NonNull Credentials credentials, @Nullable String fcmToken, @NonNull String code, @Nullable String pin, + @Nullable String basicStorageCredentials, + @Nullable TokenResponse kbsTokenResponse, @NonNull VerifyCallback callback) { new AsyncTask() { - private volatile long timeRemaining; + private volatile LockedException lockedException; + private volatile KeyBackupSystemWrongPinException keyBackupSystemWrongPinException; @Override protected Result doInBackground(Void... voids) { try { - verifyAccount(context, credentials, code, pin, fcmToken); + verifyAccount(context, credentials, code, pin, basicStorageCredentials, kbsTokenResponse, fcmToken); return Result.SUCCESS; } catch (LockedException e) { Log.w(TAG, e); - timeRemaining = e.getTimeRemaining(); + lockedException = e; return Result.PIN_LOCKED; } catch (RateLimitException e) { Log.w(TAG, e); @@ -84,36 +103,37 @@ public final class CodeVerificationRequest { } catch (IOException e) { Log.w(TAG, e); return Result.ERROR; + } catch (KeyBackupSystemWrongPinException e) { + keyBackupSystemWrongPinException = e; + return Result.KBS_WRONG_PIN; } } @Override protected void onPostExecute(Result result) { - if (result == Result.SUCCESS) { - - handleSuccessfulRegistration(context, pin); - - callback.onSuccessfulRegistration(); - } else if (result == Result.PIN_LOCKED) { - callback.onIncorrectRegistrationLockPin(timeRemaining); - } else if (result == Result.RATE_LIMITED) { - callback.onTooManyAttempts(); - } else if (result == Result.ERROR) { - callback.onError(); + switch (result) { + case SUCCESS: + handleSuccessfulRegistration(context); + callback.onSuccessfulRegistration(); + break; + case PIN_LOCKED: + callback.onIncorrectRegistrationLockPin(lockedException.getTimeRemaining(), lockedException.getBasicStorageCredentials()); + break; + case RATE_LIMITED: + callback.onTooManyAttempts(); + break; + case ERROR: + callback.onError(); + break; + case KBS_WRONG_PIN: + callback.onIncorrectKbsRegistrationLockPin(keyBackupSystemWrongPinException.getTokenResponse()); + break; } } }.execute(); } - private static void handleSuccessfulRegistration(@NonNull Context context, @Nullable String pin) { - TextSecurePreferences.setRegistrationLockPin(context, pin); - TextSecurePreferences.setRegistrationtLockEnabled(context, pin != null); - - if (pin != null) { - TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); - TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL); - } - + private static void handleSuccessfulRegistration(@NonNull Context context) { JobManager jobManager = ApplicationDependencies.getJobManager(); jobManager.add(new DirectoryRefreshJob(false)); jobManager.add(new RotateCertificateJob(context)); @@ -122,7 +142,15 @@ public final class CodeVerificationRequest { RotateSignedPreKeyListener.schedule(context); } - private static void verifyAccount(@NonNull Context context, @NonNull Credentials credentials, @NonNull String code, @Nullable String pin, @Nullable String fcmToken) throws IOException { + private static void verifyAccount(@NonNull Context context, + @NonNull Credentials credentials, + @NonNull String code, + @Nullable String pin, + @Nullable String basicStorageCredentials, + @Nullable TokenResponse kbsTokenResponse, + @Nullable String fcmToken) + throws IOException, KeyBackupSystemWrongPinException + { int registrationId = KeyHelper.generateRegistrationId(false); byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(context); boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context); @@ -130,11 +158,14 @@ public final class CodeVerificationRequest { TextSecurePreferences.setLocalRegistrationId(context, registrationId); SessionUtil.archiveAllSessions(context); - SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword()); + SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword()); + RegistrationLockData kbsData = restoreMasterKey(pin, basicStorageCredentials, kbsTokenResponse); + String registrationLock = kbsData != null ? kbsData.getMasterKey().getRegistrationLock() : null; + boolean present = fcmToken != null; - boolean present = fcmToken != null; - - UUID uuid = accountManager.verifyAccountWithCode(code, null, registrationId, !present, pin, unidentifiedAccessKey, universalUnidentifiedAccess); + UUID uuid = accountManager.verifyAccountWithCode(code, null, registrationId, !present, + pin, registrationLock, + unidentifiedAccessKey, universalUnidentifiedAccess); IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context); List records = PreKeyUtil.generatePreKeys(context); @@ -170,6 +201,85 @@ public final class CodeVerificationRequest { TextSecurePreferences.setSignedPreKeyRegistered(context, true); TextSecurePreferences.setPromptedPushRegistration(context, true); TextSecurePreferences.setUnauthorizedReceived(context, false); + TextSecurePreferences.setRegistrationLockMasterKey(context, kbsData, System.currentTimeMillis()); + if (kbsData == null) { + //noinspection deprecation Only acceptable place to write the old pin. + TextSecurePreferences.setDeprecatedRegistrationLockPin(context, pin); + if (pin != null) { + if (FeatureFlags.KBS) { + Log.i(TAG, "Pin V1 successfully entered during registration, scheduling a migration to Pin V2"); + ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob()); + } + } + } else { + repostPinToResetTries(context, pin, kbsData); + } + TextSecurePreferences.setRegistrationLockEnabled(context, pin != null); + if (pin != null) { + TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); + TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL); + } + } + + private static void repostPinToResetTries(@NonNull Context context, @Nullable String pin, @NonNull RegistrationLockData kbsData) { + if (!FeatureFlags.KBS) return; + + KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); + + try { + RegistrationLockData newData = keyBackupService.newPinChangeSession(kbsData.getTokenResponse()) + .setPin(pin); + TextSecurePreferences.setRegistrationLockMasterKey(context, newData, System.currentTimeMillis()); + } catch (IOException e) { + Log.w(TAG, "May have failed to reset pin attempts!", e); + } catch (UnauthenticatedResponseException e) { + Log.w(TAG, "Failed to reset pin attempts", e); + } catch (InvalidPinException e) { + throw new AssertionError(e); + } + } + + private static @Nullable RegistrationLockData restoreMasterKey(@Nullable String pin, + @Nullable String basicStorageCredentials, + @Nullable TokenResponse tokenResponse) + throws IOException, KeyBackupSystemWrongPinException + { + if (pin == null) return null; + + if (basicStorageCredentials == null) { + Log.i(TAG, "No storage credentials supplied, pin is not on KBS"); + return null; + } + + if (!FeatureFlags.KBS) { + Log.w(TAG, "User appears to have a KBS pin, but this build has KBS off."); + return null; + } + + KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); + + Log.i(TAG, "Opening key backup service session"); + KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials, tokenResponse); + + try { + Log.i(TAG, "Restoring pin from KBS"); + RegistrationLockData kbsData = session.restorePin(pin); + if (kbsData != null) { + Log.i(TAG, "Found registration lock token on KBS."); + } else { + Log.i(TAG, "No KBS data found."); + } + return kbsData; + } catch (UnauthenticatedResponseException e) { + Log.w(TAG, "Failed to restore key", e); + throw new IOException(e); + } catch (KeyBackupServicePinException e) { + Log.w(TAG, "Incorrect pin", e); + throw new KeyBackupSystemWrongPinException(e.getToken()); + } catch (InvalidPinException e) { + Log.w(TAG, "Invalid pin", e); + return null; + } } public interface VerifyCallback { @@ -179,7 +289,9 @@ public final class CodeVerificationRequest { /** * @param timeRemaining Time until pin expires and number can be reused. */ - void onIncorrectRegistrationLockPin(long timeRemaining); + void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials); + + void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse kbsTokenResponse); void onTooManyAttempts(); diff --git a/src/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java b/src/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java new file mode 100644 index 0000000000..8aadbb4640 --- /dev/null +++ b/src/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java @@ -0,0 +1,18 @@ +package org.thoughtcrime.securesms.registration.service; + +import androidx.annotation.NonNull; + +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; + +final class KeyBackupSystemWrongPinException extends Exception { + + private final TokenResponse tokenResponse; + + KeyBackupSystemWrongPinException(@NonNull TokenResponse tokenResponse){ + this.tokenResponse = tokenResponse; + } + + @NonNull TokenResponse getTokenResponse() { + return tokenResponse; + } +} diff --git a/src/org/thoughtcrime/securesms/registration/service/RegistrationService.java b/src/org/thoughtcrime/securesms/registration/service/RegistrationService.java index 70061ca23b..37a0737361 100644 --- a/src/org/thoughtcrime/securesms/registration/service/RegistrationService.java +++ b/src/org/thoughtcrime/securesms/registration/service/RegistrationService.java @@ -5,6 +5,10 @@ import android.app.Activity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; + +import java.io.IOException; + public final class RegistrationService { private final Credentials credentials; @@ -35,8 +39,14 @@ public final class RegistrationService { @Nullable String fcmToken, @NonNull String code, @Nullable String pin, + @Nullable String basicStorageCredentials, + @Nullable TokenResponse tokenResponse, @NonNull CodeVerificationRequest.VerifyCallback callback) { - CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, code, pin, callback); + CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, code, pin, basicStorageCredentials, tokenResponse, callback); + } + + public @Nullable TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException { + return CodeVerificationRequest.getToken(basicStorageCredentials); } } diff --git a/src/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java b/src/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java index 0a97aa4f60..db5513b44a 100644 --- a/src/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java +++ b/src/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java @@ -4,22 +4,38 @@ import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.SavedStateHandle; +import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.registration.service.RegistrationService; import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.concurrent.SimpleTask; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.util.JsonUtil; + +import java.io.IOException; public final class RegistrationViewModel extends ViewModel { + private static final String TAG = Log.tag(RegistrationViewModel.class); + private final String secret; private final MutableLiveData number; private final MutableLiveData textCodeEntered; private final MutableLiveData captchaToken; private final MutableLiveData fcmToken; + private final MutableLiveData basicStorageCredentials; private final MutableLiveData restoreFlowShown; private final MutableLiveData successfulCodeRequestAttempts; private final MutableLiveData requestLimiter; + private final MutableLiveData keyBackupcurrentTokenJson; + private final LiveData keyBackupcurrentToken; + private final LiveData> tokenResponseCredentialsPair; public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) { secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18)); @@ -28,9 +44,24 @@ public final class RegistrationViewModel extends ViewModel { textCodeEntered = savedStateHandle.getLiveData("TEXT_CODE_ENTERED", ""); captchaToken = savedStateHandle.getLiveData("CAPTCHA"); fcmToken = savedStateHandle.getLiveData("FCM_TOKEN"); + basicStorageCredentials = savedStateHandle.getLiveData("BASIC_STORAGE_CREDENTIALS"); restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false); successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0); requestLimiter = savedStateHandle.getLiveData("REQUEST_RATE_LIMITER", new LocalCodeRequestRateLimiter(60_000)); + keyBackupcurrentTokenJson = savedStateHandle.getLiveData("KBS_TOKEN"); + + keyBackupcurrentToken = Transformations.map(keyBackupcurrentTokenJson, json -> + { + if (json == null) return null; + try { + return JsonUtil.fromJson(json, TokenResponse.class); + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + }); + + tokenResponseCredentialsPair = new LiveDataPair<>(keyBackupcurrentToken, basicStorageCredentials); } private static T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) { @@ -138,4 +169,59 @@ public final class RegistrationViewModel extends ViewModel { public void updateLimiter() { requestLimiter.setValue(requestLimiter.getValue()); } + + public void setStorageCredentials(@Nullable String storageCredentials) { + basicStorageCredentials.setValue(storageCredentials); + } + + public @Nullable String getBasicStorageCredentials() { + return basicStorageCredentials.getValue(); + } + + public @Nullable TokenResponse getKeyBackupCurrentToken() { + return keyBackupcurrentToken.getValue(); + } + + public void setKeyBackupCurrentToken(TokenResponse tokenResponse) { + keyBackupcurrentTokenJson.setValue(tokenResponse == null ? null : JsonUtil.toJson(tokenResponse)); + } + + public LiveData> getTokenResponseCredentialsPair() { + return tokenResponseCredentialsPair; + } + + public void onRegistrationLockFragmentCreate() { + SimpleTask.run(() -> { + RegistrationService registrationService = RegistrationService.getInstance(getNumber().getE164Number(), getRegistrationSecret()); + try { + return registrationService.getToken(getBasicStorageCredentials()); + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + }, this::setKeyBackupCurrentToken); + } + + public static class LiveDataPair extends MediatorLiveData> { + private A a; + private B b; + + public LiveDataPair(LiveData ld1, LiveData ld2) { + setValue(new Pair<>(a, b)); + + addSource(ld1, (a) -> { + if(a != null) { + this.a = a; + } + setValue(new Pair<>(a, b)); + }); + + addSource(ld2, (b) -> { + if(b != null) { + this.b = b; + } + setValue(new Pair<>(a, b)); + }); + } +} } diff --git a/src/org/thoughtcrime/securesms/util/FeatureFlags.java b/src/org/thoughtcrime/securesms/util/FeatureFlags.java index d80fe4b1de..4f6823cb6e 100644 --- a/src/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/src/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -19,4 +19,7 @@ public class FeatureFlags { /** Creating usernames, sending messages by username. Requires {@link #UUIDS}. */ public static final boolean USERNAMES = false; + + /** Set or migrate PIN to KBS */ + public static final boolean KBS = false; } diff --git a/src/org/thoughtcrime/securesms/util/IasKeyStore.java b/src/org/thoughtcrime/securesms/util/IasKeyStore.java new file mode 100644 index 0000000000..a6ce00aef7 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/IasKeyStore.java @@ -0,0 +1,33 @@ +package org.thoughtcrime.securesms.util; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.push.IasTrustStore; +import org.whispersystems.signalservice.api.push.TrustStore; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public final class IasKeyStore { + + private IasKeyStore() { + } + + public static KeyStore getIasKeyStore(@NonNull Context context) { + try { + TrustStore contactTrustStore = new IasTrustStore(context); + + KeyStore keyStore = KeyStore.getInstance("BKS"); + keyStore.load(contactTrustStore.getKeyStoreInputStream(), contactTrustStore.getKeyStorePassword().toCharArray()); + + return keyStore; + } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } +} diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index ce5035905f..982855cf82 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -7,6 +7,8 @@ import android.net.Uri; import android.os.Build; import android.preference.PreferenceManager; import android.provider.Settings; +import android.text.TextUtils; + import androidx.annotation.ArrayRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,7 +21,10 @@ import org.thoughtcrime.securesms.lock.RegistrationLockReminders; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; import org.whispersystems.libsignal.util.Medium; +import org.whispersystems.signalservice.api.RegistrationLockData; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.registrationpin.PinStretcher; import java.io.IOException; import java.security.SecureRandom; @@ -154,8 +159,14 @@ public class TextSecurePreferences { public static final String REGISTRATION_LOCK_PREF = "pref_registration_lock"; private static final String REGISTRATION_LOCK_PIN_PREF = "pref_registration_lock_pin"; + private static final String REGISTRATION_LOCK_TOKEN_PREF = "pref_registration_lock_token"; + private static final String REGISTRATION_LOCK_PIN_KEY_2_PREF = "pref_registration_lock_pin_key_2"; + private static final String REGISTRATION_LOCK_MASTER_KEY = "pref_registration_lock_master_key"; + private static final String REGISTRATION_LOCK_TOKEN_RESPONSE = "pref_registration_lock_token_response"; private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME = "pref_registration_lock_last_reminder_time"; private static final String REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL = "pref_registration_lock_next_reminder_interval"; + private static final String REGISTRATION_LOCK_SERVER_CONSISTENT = "pref_registration_lock_server_consistent"; + private static final String REGISTRATION_LOCK_SERVER_CONSISTENT_TIME = "pref_registration_lock_server_consistent_time"; private static final String SERVICE_OUTAGE = "pref_service_outage"; private static final String LAST_OUTAGE_CHECK_TIME = "pref_last_outage_check_time"; @@ -218,22 +229,131 @@ public class TextSecurePreferences { setLongPreference(context, SCREEN_LOCK_TIMEOUT, value); } - public static boolean isRegistrationtLockEnabled(@NonNull Context context) { + public static boolean isRegistrationLockEnabled(@NonNull Context context) { return getBooleanPreference(context, REGISTRATION_LOCK_PREF, false); } - public static void setRegistrationtLockEnabled(@NonNull Context context, boolean value) { + public static void setRegistrationLockEnabled(@NonNull Context context, boolean value) { setBooleanPreference(context, REGISTRATION_LOCK_PREF, value); } - public static @Nullable String getRegistrationLockPin(@NonNull Context context) { + /** + * @deprecated Use only for migrations to the Key Backup Store registration pinV2. + */ + @Deprecated + public static @Nullable String getDeprecatedRegistrationLockPin(@NonNull Context context) { return getStringPreference(context, REGISTRATION_LOCK_PIN_PREF, null); } - public static void setRegistrationLockPin(@NonNull Context context, String pin) { + public static boolean hasOldRegistrationLockPin(@NonNull Context context) { + //noinspection deprecation + return !TextUtils.isEmpty(getDeprecatedRegistrationLockPin(context)); + } + + public static void clearOldRegistrationLockPin(@NonNull Context context) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .remove(REGISTRATION_LOCK_PIN_PREF) + .apply(); + } + + /** + * @deprecated Use only for migrations to the Key Backup Store registration pinV2. + */ + @Deprecated + public static void setDeprecatedRegistrationLockPin(@NonNull Context context, String pin) { setStringPreference(context, REGISTRATION_LOCK_PIN_PREF, pin); } + /** Clears old pin preference at same time if non-null */ + public static void setRegistrationLockMasterKey(@NonNull Context context, @Nullable RegistrationLockData registrationLockData, long time) { + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(REGISTRATION_LOCK_SERVER_CONSISTENT, true) + .putLong(REGISTRATION_LOCK_SERVER_CONSISTENT_TIME, time); + + if (registrationLockData == null) { + editor.remove(REGISTRATION_LOCK_TOKEN_RESPONSE) + .remove(REGISTRATION_LOCK_MASTER_KEY) + .remove(REGISTRATION_LOCK_TOKEN_PREF) + .remove(REGISTRATION_LOCK_PIN_KEY_2_PREF); + } else { + PinStretcher.MasterKey masterKey = registrationLockData.getMasterKey(); + String tokenResponse; + try { + tokenResponse = JsonUtils.toJson(registrationLockData.getTokenResponse()); + } catch (IOException e) { + throw new AssertionError(e); + } + + editor.remove(REGISTRATION_LOCK_PIN_PREF) // Removal of V1 pin + .putBoolean(REGISTRATION_LOCK_PREF, true) + .putString(REGISTRATION_LOCK_TOKEN_RESPONSE, tokenResponse) + .putString(REGISTRATION_LOCK_MASTER_KEY, Base64.encodeBytes(masterKey.getMasterKey())) + .putString(REGISTRATION_LOCK_TOKEN_PREF, masterKey.getRegistrationLock()) + .putString(REGISTRATION_LOCK_PIN_KEY_2_PREF, Base64.encodeBytes(masterKey.getPinKey2())); + } + + editor.apply(); + } + + public static byte[] getMasterKey(@NonNull Context context) { + String key = getStringPreference(context, REGISTRATION_LOCK_MASTER_KEY, null); + if (key == null) { + return null; + } + try { + return Base64.decode(key); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + public static void setRegistrationLockServerConsistent(@NonNull Context context, boolean consistent, long time) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(REGISTRATION_LOCK_SERVER_CONSISTENT, consistent) + .putLong(REGISTRATION_LOCK_SERVER_CONSISTENT_TIME, time) + .apply(); + } + + public static @Nullable String getRegistrationLockToken(@NonNull Context context) { + return getStringPreference(context, REGISTRATION_LOCK_TOKEN_PREF, null); + } + + public static void setRegistrationLockTokenResponse(@NonNull Context context, @NonNull TokenResponse tokenResponse) { + String tokenResponseString; + try { + tokenResponseString = JsonUtils.toJson(tokenResponse); + } catch (IOException e) { + throw new AssertionError(e); + } + setStringPreference(context, REGISTRATION_LOCK_TOKEN_RESPONSE, tokenResponseString); + } + + public static @Nullable TokenResponse getRegistrationLockTokenResponse(@NonNull Context context) { + String token = getStringPreference(context, REGISTRATION_LOCK_TOKEN_RESPONSE, null); + + if (token == null) return null; + + try { + return JsonUtils.fromJson(token, TokenResponse.class); + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + } + + public static @Nullable byte[] getRegistrationLockPinKey2(@NonNull Context context){ + String pinKey2 = getStringPreference(context, REGISTRATION_LOCK_PIN_KEY_2_PREF, null); + try { + return pinKey2 != null ? Base64.decode(pinKey2) : null; + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + } + public static long getRegistrationLockLastReminderTime(@NonNull Context context) { return getLongPreference(context, REGISTRATION_LOCK_LAST_REMINDER_TIME, 0); }