Add internal pre-alpha support for Registration Lock v2.

master
Alan Evans 2019-12-03 12:31:23 -05:00 committed by Greyson Parrelli
parent 058c25808b
commit 7f8ca58762
50 changed files with 2313 additions and 340 deletions

View File

@ -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 {

View File

@ -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'
}

View File

@ -0,0 +1,75 @@
/**
* Copyright (C) 2019 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package textsecure;
option java_package = "org.whispersystems.signalservice.internal.keybackup.protos";
option java_multiple_files = true;
message Request {
optional BackupRequest backup = 1;
optional RestoreRequest restore = 2;
optional DeleteRequest delete = 3;
}
message Response {
optional BackupResponse backup = 1;
optional RestoreResponse restore = 2;
optional DeleteResponse delete = 3;
}
message BackupRequest {
optional bytes service_id = 1;
optional bytes backup_id = 2;
optional bytes token = 3;
optional uint64 valid_from = 4;
optional bytes data = 5;
optional bytes pin = 6;
optional uint32 tries = 7;
}
message BackupResponse {
enum Status {
OK = 1;
ALREADY_EXISTS = 2;
NOT_YET_VALID = 3;
}
optional Status status = 1;
optional bytes token = 2;
}
message RestoreRequest {
optional bytes service_id = 1;
optional bytes backup_id = 2;
optional bytes token = 3;
optional uint64 valid_from = 4;
optional bytes pin = 5;
}
message RestoreResponse {
enum Status {
OK = 1;
TOKEN_MISMATCH = 2;
NOT_YET_VALID = 3;
MISSING = 4;
PIN_MISMATCH = 5;
}
optional Status status = 1;
optional bytes token = 2;
optional bytes data = 3;
optional uint32 tries = 4;
}
message DeleteRequest {
optional bytes service_id = 1;
optional bytes backup_id = 2;
}
message DeleteResponse {
}

View File

@ -0,0 +1,281 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse;
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.IOException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.util.Locale;
public final class KeyBackupService {
private static final String TAG = KeyBackupService.class.getSimpleName();
private final KeyStore iasKeyStore;
private final String enclaveName;
private final String mrenclave;
private final PushServiceSocket pushServiceSocket;
private final int maxTries;
KeyBackupService(KeyStore iasKeyStore,
String enclaveName,
String mrenclave,
PushServiceSocket pushServiceSocket,
int maxTries)
{
this.iasKeyStore = iasKeyStore;
this.enclaveName = enclaveName;
this.mrenclave = mrenclave;
this.pushServiceSocket = pushServiceSocket;
this.maxTries = maxTries;
}
/**
* Use this if you don't want to validate that the server has not changed since you last set the pin.
*/
public PinChangeSession newPinChangeSession()
throws IOException
{
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), null);
}
/**
* Use this if you want to validate that the server has not changed since you last set the pin.
* The supplied token will have to match for the change to be successful.
*/
public PinChangeSession newPinChangeSession(TokenResponse currentToken)
throws IOException
{
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
}
/**
* Use this to validate that the pin is still set on the server with the current token.
* Additionally this validates that no one has used any tries.
*/
public RestoreSession newRestoreSession(TokenResponse currentToken)
throws IOException
{
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
}
/**
* Only call before registration, to see how many tries are left.
* <p>
* Pass the token to the newRegistrationSession.
*/
public TokenResponse getToken(String authAuthorization) throws IOException {
return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
}
/**
* Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt.
*
* @param tokenResponse Supplying a token response from a failed previous attempt prevents certain attacks.
*/
public RestoreSession newRegistrationSession(String authAuthorization, TokenResponse tokenResponse)
throws IOException
{
return newSession(authAuthorization, tokenResponse);
}
private Session newSession(String authorization, TokenResponse currentToken)
throws IOException
{
TokenResponse token = currentToken != null ? currentToken : pushServiceSocket.getKeyBackupServiceToken(authorization, enclaveName);
return new Session(authorization, token);
}
private class Session implements RestoreSession, PinChangeSession {
private final String authorization;
private final TokenResponse currentToken;
Session(String authorization, TokenResponse currentToken) {
this.authorization = authorization;
this.currentToken = currentToken;
}
@Override
public RegistrationLockData restorePin(String pin)
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException
{
int attempt = 0;
SecureRandom random = new SecureRandom();
TokenResponse token = currentToken;
while (true) {
attempt++;
try {
return restorePin(pin, token);
} catch (TokenException tokenException) {
token = tokenException.getToken();
if (tokenException instanceof KeyBackupServicePinException) {
throw (KeyBackupServicePinException) tokenException;
}
if (tokenException.isCanAutomaticallyRetry() && attempt < 5) {
// back off randomly, between 250 and 8000 ms
int backoffMs = 250 * (1 << (attempt - 1));
Util.sleep(backoffMs + random.nextInt(backoffMs));
} else {
throw new UnauthenticatedResponseException("Token mismatch, expended all automatic retries");
}
}
}
}
private RegistrationLockData restorePin(String pin, TokenResponse token)
throws UnauthenticatedResponseException, IOException, TokenException, InvalidPinException
{
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
try {
final int remainingTries = token.getTries();
final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(stretchedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName));
final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation);
TokenResponse nextToken = status.hasToken()
? new TokenResponse(token.getBackupId(), status.getToken().toByteArray(), status.getTries())
: token;
Log.i(TAG, "Restore " + status.getStatus());
switch (status.getStatus()) {
case OK:
Log.i(TAG, String.format(Locale.US,"Restore OK! data: %s tries: %d", Hex.toStringCondensed(status.getData().toByteArray()), status.getTries()));
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(status.getData().toByteArray());
return new RegistrationLockData(masterKey, nextToken);
case PIN_MISMATCH:
Log.i(TAG, "Restore PIN_MISMATCH");
throw new KeyBackupServicePinException(nextToken);
case TOKEN_MISMATCH:
Log.i(TAG, "Restore TOKEN_MISMATCH");
// if the number of tries has not fallen, the pin is correct we're just using an out of date token
boolean canRetry = remainingTries == status.getTries();
Log.i(TAG, String.format(Locale.US, "Token MISMATCH %d %d", remainingTries, status.getTries()));
Log.i(TAG, String.format("Last token %s", Hex.toStringCondensed(token.getToken())));
Log.i(TAG, String.format("Next token %s", Hex.toStringCondensed(nextToken.getToken())));
throw new TokenException(nextToken, canRetry);
case MISSING:
Log.i(TAG, "Restore OK! No data though");
return null;
case NOT_YET_VALID:
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
}
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
return null;
}
private RemoteAttestation getAndVerifyRemoteAttestation() throws UnauthenticatedResponseException, IOException {
try {
return RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.KeyBackup, iasKeyStore, enclaveName, mrenclave, authorization);
} catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | InvalidCiphertextException | SignatureException e) {
throw new UnauthenticatedResponseException(e);
}
}
@Override
public RegistrationLockData setPin(String pin) throws IOException, UnauthenticatedResponseException, InvalidPinException {
PinStretcher.MasterKey masterKey = PinStretcher.stretchPin(pin)
.withNewSecurePinKey2();
TokenResponse tokenResponse = putKbsData(masterKey.getKbsAccessKey(),
masterKey.getPinKey2(),
enclaveName,
currentToken);
pushServiceSocket.setRegistrationLock(masterKey.getRegistrationLock());
return new RegistrationLockData(masterKey, tokenResponse);
}
@Override
public void removePin() throws IOException, UnauthenticatedResponseException {
deleteKbsData();
pushServiceSocket.removePinV2();
}
private TokenResponse putKbsData(byte[] kbsAccessKey, byte[] kbsData, String enclaveName, TokenResponse token)
throws IOException, UnauthenticatedResponseException
{
try {
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
KeyBackupRequest request = KeyBackupCipher.createKeyBackupRequest(kbsAccessKey, kbsData, token, remoteAttestation, Hex.fromStringCondensed(enclaveName), maxTries);
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
BackupResponse backupResponse = KeyBackupCipher.getKeyBackupResponse(response, remoteAttestation);
BackupResponse.Status status = backupResponse.getStatus();
switch (status) {
case OK:
return backupResponse.hasToken() ? new TokenResponse(token.getBackupId(), backupResponse.getToken().toByteArray(), maxTries) : token;
case ALREADY_EXISTS:
throw new UnauthenticatedResponseException("Already exists");
case NOT_YET_VALID:
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
default:
throw new AssertionError("Unknown response status " + status);
}
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
}
private void deleteKbsData()
throws IOException, UnauthenticatedResponseException
{
try {
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
KeyBackupRequest request = KeyBackupCipher.createKeyDeleteRequest(currentToken, remoteAttestation, Hex.fromStringCondensed(enclaveName));
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
KeyBackupCipher.getKeyDeleteResponseStatus(response, remoteAttestation);
} catch (InvalidCiphertextException e) {
throw new UnauthenticatedResponseException(e);
}
}
}
public interface RestoreSession {
RegistrationLockData restorePin(String pin)
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException;
}
public interface PinChangeSession {
RegistrationLockData setPin(String pin)
throws IOException, UnauthenticatedResponseException, InvalidPinException;
void removePin()
throws IOException, UnauthenticatedResponseException;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -9,8 +9,6 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@ -18,7 +16,6 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
@ -33,23 +30,19 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.MessageDigest;
@ -117,18 +110,34 @@ public class SignalServiceAccountManager {
return this.pushServiceSocket.getSenderCertificateLegacy();
}
public void setPin(Optional<String> pin) throws IOException {
if (pin.isPresent()) {
this.pushServiceSocket.setPin(pin.get());
} else {
this.pushServiceSocket.removePin();
}
/**
* @deprecated Remove this method once KBS is live.
*/
@Deprecated
public void setPin(String pin) throws IOException {
this.pushServiceSocket.setPin(pin);
}
/**
* V1 Pin setting has been replaced by KeyBackupService.
* Now you can only remove the old pin but there is no need to remove the old pin if setting a KBS Pin.
*/
public void removeV1Pin() throws IOException {
this.pushServiceSocket.removePin();
}
public UUID getOwnUuid() throws IOException {
return this.pushServiceSocket.getOwnUuid();
}
public KeyBackupService getKeyBackupService(KeyStore iasKeyStore,
String enclaveName,
String mrenclave,
int tries)
{
return new KeyBackupService(iasKeyStore, enclaveName, mrenclave, pushServiceSocket, tries);
}
/**
* Register/Unregister a Google Cloud Messaging registration ID.
*
@ -193,16 +202,20 @@ public class SignalServiceAccountManager {
* This value should remain consistent across registrations for the
* same install, but probabilistically differ across registrations
* for separate installs.
* @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS.
* @param registrationLock Only supply if found on KBS.
* @return The UUID of the user that was registered.
* @throws IOException
*/
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
signalProtocolRegistrationId,
fetchesMessages, pin,
fetchesMessages,
pin, registrationLock,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess);
}
@ -215,14 +228,18 @@ public class SignalServiceAccountManager {
* This value should remain consistent across registrations for the same
* install, but probabilistically differ across registrations for
* separate installs.
* @param pin Only supply if pin has not yet been migrated to KBS.
* @param registrationLock Only supply if found on KBS.
*
* @throws IOException
*/
public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages, pin,
this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages,
pin, registrationLock,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
}
@ -306,35 +323,21 @@ public class SignalServiceAccountManager {
return activeTokens;
}
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String mrenclave)
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String enclaveId)
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
{
try {
String authorization = this.pushServiceSocket.getContactDiscoveryAuthorization();
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
Curve25519KeyPair keyPair = curve.generateKeyPair();
ContactDiscoveryCipher cipher = new ContactDiscoveryCipher();
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
Pair<RemoteAttestationResponse, List<String>> attestationResponse = this.pushServiceSocket.getContactDiscoveryRemoteAttestation(authorization, attestationRequest, mrenclave);
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.first().getServerEphemeralPublic(), attestationResponse.first().getServerStaticPublic());
Quote quote = new Quote(attestationResponse.first().getQuote());
byte[] requestId = cipher.getRequestId(keys, attestationResponse.first());
cipher.verifyServerQuote(quote, attestationResponse.first().getServerStaticPublic(), mrenclave);
cipher.verifyIasSignature(iasKeyStore, attestationResponse.first().getCertificates(), attestationResponse.first().getSignatureBody(), attestationResponse.first().getSignature(), quote);
RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys);
List<String> addressBook = new LinkedList<>();
String authorization = pushServiceSocket.getContactDiscoveryAuthorization();
RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization);
List<String> addressBook = new LinkedList<>();
for (String e164number : e164numbers) {
addressBook.add(e164number.substring(1));
}
DiscoveryRequest request = cipher.createDiscoveryRequest(addressBook, remoteAttestation);
DiscoveryResponse response = this.pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, attestationResponse.second(), mrenclave);
byte[] data = cipher.getDiscoveryResponseData(response, remoteAttestation);
DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, remoteAttestation);
DiscoveryResponse response = pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, remoteAttestation.getCookies(), enclaveId);
byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, remoteAttestation);
Iterator<String> addressBookIterator = addressBook.iterator();
List<String> results = new LinkedList<>();

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -1,47 +1,20 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import org.threeten.bp.Instant;
import org.threeten.bp.LocalDateTime;
import org.threeten.bp.Period;
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.format.DateTimeFormatter;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public final class ContactDiscoveryCipher {
public class ContactDiscoveryCipher {
private ContactDiscoveryCipher() {
}
private static final int TAG_LENGTH_BYTES = 16;
private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8;
private static final long SIGNATURE_BODY_VERSION = 3L;
public DiscoveryRequest createDiscoveryRequest(List<String> addressBook, RemoteAttestation remoteAttestation) {
public static DiscoveryRequest createDiscoveryRequest(List<String> addressBook, RemoteAttestation remoteAttestation) {
try {
ByteArrayOutputStream requestDataStream = new ByteArrayOutputStream();
@ -49,100 +22,19 @@ public class ContactDiscoveryCipher {
requestDataStream.write(ByteUtil.longToByteArray(Long.parseLong(address)));
}
byte[] requestData = requestDataStream.toByteArray();
byte[] nonce = Util.getSecretBytes(12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] clientKey = remoteAttestation.getKeys().getClientKey();
byte[] requestData = requestDataStream.toByteArray();
byte[] aad = remoteAttestation.getRequestId();
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(remoteAttestation.getKeys().getClientKey(), "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, nonce));
cipher.updateAAD(remoteAttestation.getRequestId());
AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData);
byte[] cipherText = cipher.doFinal(requestData);
byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);
return new DiscoveryRequest(addressBook.size(), remoteAttestation.getRequestId(), nonce, parts[0], parts[1]);
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
public byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
return decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
}
public byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException {
return decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag());
}
public void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave)
throws UnauthenticatedQuoteException
{
try {
byte[] theirServerPublicStatic = new byte[serverPublicStatic.length];
System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length);
if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) {
throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!");
}
if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) {
throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave()));
}
if (quote.isDebugQuote()) {
throw new UnauthenticatedQuoteException("Received quote for debuggable enclave");
}
return new DiscoveryRequest(addressBook.size(), aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac);
} catch (IOException e) {
throw new UnauthenticatedQuoteException(e);
}
}
public void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote)
throws SignatureException
{
if (certificates == null || certificates.isEmpty()) {
throw new SignatureException("No certificates.");
}
try {
SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore);
signingCertificate.verifySignature(signatureBody, signature);
SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class);
if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) {
throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion());
}
if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) {
throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes()));
}
if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) {
throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus());
}
if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC")))
.plus(Period.ofDays(1))
.isBefore(Instant.now()))
{
throw new SignatureException("Signature is expired");
}
} catch (CertificateException | CertPathValidatorException | IOException e) {
throw new SignatureException(e);
}
}
private byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
return cipher.doFinal(ByteUtil.combine(ciphertext, tag));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new AssertionError(e);
} catch (InvalidKeyException | BadPaddingException e) {
throw new InvalidCiphertextException(e);
}
}
public static byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
}
}

View File

@ -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);
}
}

View File

@ -1,13 +1,17 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import java.util.List;
public class RemoteAttestation {
private final byte[] requestId;
private final RemoteAttestationKeys keys;
private final List<String> cookies;
public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys) {
public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys, List<String> cookies) {
this.requestId = requestId;
this.keys = keys;
this.cookies = cookies;
}
public byte[] getRequestId() {
@ -17,4 +21,8 @@ public class RemoteAttestation {
public RemoteAttestationKeys getKeys() {
return keys;
}
public List<String> getCookies() {
return cookies;
}
}

View File

@ -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);
}
}
}

View File

@ -31,11 +31,15 @@ public class SigningCertificate {
{
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) certificateFactory.generateCertificates(new ByteArrayInputStream(URLDecoder.decode(certificateChain).getBytes()));
Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) certificateFactory.generateCertificates(new ByteArrayInputStream(certificateChain.getBytes()));
List<X509Certificate> certificates = new LinkedList<>(certificatesCollection);
PKIXParameters pkixParameters = new PKIXParameters(trustStore);
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
if (certificates.isEmpty()) {
throw new CertificateException("No certificates available! Badly-formatted cert chain?");
}
this.path = certificateFactory.generateCertPath(certificates);
pkixParameters.setRevocationEnabled(false);

View File

@ -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() {

View File

@ -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) + "}";
}
}

View File

@ -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)) + "}";
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -43,8 +43,9 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.configuration.SignalUrl;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
@ -78,14 +79,15 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.Call;
import okhttp3.ConnectionSpec;
import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
@ -108,6 +110,7 @@ public class PushServiceSocket {
private static final String TURN_SERVER_INFO = "/v1/accounts/turn";
private static final String SET_ACCOUNT_ATTRIBUTES = "/v1/accounts/attributes/";
private static final String PIN_PATH = "/v1/accounts/pin/";
private static final String REGISTRATION_LOCK_PATH = "/v1/accounts/registration_lock";
private static final String REQUEST_PUSH_CHALLENGE = "/v1/accounts/fcm/preauth/%s/%s";
private static final String WHO_AM_I = "/v1/accounts/whoami";
private static final String SET_USERNAME_PATH = "/v1/accounts/username/%s";
@ -137,6 +140,8 @@ public class PushServiceSocket {
private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery";
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true";
private static final String KBS_AUTH_PATH = "/v1/backup/auth";
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
@ -152,6 +157,7 @@ public class PushServiceSocket {
private final ServiceConnectionHolder[] serviceClients;
private final ConnectionHolder[] cdnClients;
private final ConnectionHolder[] contactDiscoveryClients;
private final ConnectionHolder[] keyBackupServiceClients;
private final OkHttpClient attachmentClient;
private final CredentialsProvider credentialsProvider;
@ -164,6 +170,7 @@ public class PushServiceSocket {
this.serviceClients = createServiceConnectionHolders(signalServiceConfiguration.getSignalServiceUrls());
this.cdnClients = createConnectionHolders(signalServiceConfiguration.getSignalCdnUrls());
this.contactDiscoveryClients = createConnectionHolders(signalServiceConfiguration.getSignalContactDiscoveryUrls());
this.keyBackupServiceClients = createConnectionHolders(signalServiceConfiguration.getSignalKeyBackupServiceUrls());
this.attachmentClient = createAttachmentClient();
this.random = new SecureRandom();
}
@ -219,11 +226,12 @@ public class PushServiceSocket {
}
}
public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, String pin,
public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
String requestBody = JsonUtil.toJson(signalingKeyEntity);
String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody);
VerifyAccountResponse response = JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
@ -236,11 +244,16 @@ public class PushServiceSocket {
}
}
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin,
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin,
if (registrationLock != null && pin != null) {
throw new AssertionError("Pin should be null if registrationLock is set.");
}
AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes));
}
@ -282,10 +295,20 @@ public class PushServiceSocket {
makeServiceRequest(PIN_PATH, "PUT", JsonUtil.toJson(accountLock));
}
/** Note: Setting a KBS Pin will clear this */
public void removePin() throws IOException {
makeServiceRequest(PIN_PATH, "DELETE", null);
}
public void setRegistrationLock(String registrationLock) throws IOException {
RegistrationLockV2 accountLock = new RegistrationLockV2(registrationLock);
makeServiceRequest(REGISTRATION_LOCK_PATH, "PUT", JsonUtil.toJson(accountLock));
}
public void removePinV2() throws IOException {
makeServiceRequest(REGISTRATION_LOCK_PATH, "DELETE", null);
}
public byte[] getSenderCertificateLegacy() throws IOException {
String responseText = makeServiceRequest(SENDER_CERTIFICATE_LEGACY_PATH, "GET", null);
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
@ -592,26 +615,27 @@ public class PushServiceSocket {
}
}
public String getContactDiscoveryAuthorization() throws IOException {
String response = makeServiceRequest(DIRECTORY_AUTH_PATH, "GET", null);
ContactDiscoveryCredentials token = JsonUtil.fromJson(response, ContactDiscoveryCredentials.class);
return Credentials.basic(token.getUsername(), token.getPassword());
private String getCredentials(String authPath) throws IOException {
String response = makeServiceRequest(authPath, "GET", null, NO_HEADERS);
AuthCredentials token = JsonUtil.fromJson(response, AuthCredentials.class);
return token.asBasic();
}
public Pair<RemoteAttestationResponse, List<String>> getContactDiscoveryRemoteAttestation(String authorization, RemoteAttestationRequest request, String mrenclave)
throws IOException
{
Response response = makeContactDiscoveryRequest(authorization, new LinkedList<String>(), "/v1/attestation/" + mrenclave, "PUT", JsonUtil.toJson(request));
ResponseBody body = response.body();
List<String> rawCookies = response.headers("Set-Cookie");
List<String> cookies = new LinkedList<>();
public String getContactDiscoveryAuthorization() throws IOException {
return getCredentials(DIRECTORY_AUTH_PATH);
}
for (String cookie : rawCookies) {
cookies.add(cookie.split(";")[0]);
}
public String getKeyBackupServiceAuthorization() throws IOException {
return getCredentials(KBS_AUTH_PATH);
}
public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName)
throws IOException
{
ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, null, "/v1/token/" + enclaveName, "GET", null).body();
if (body != null) {
return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
return JsonUtil.fromJson(body.string(), TokenResponse.class);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
@ -620,7 +644,7 @@ public class PushServiceSocket {
public DiscoveryResponse getContactDiscoveryRegisteredUsers(String authorizationToken, DiscoveryRequest request, List<String> cookies, String mrenclave)
throws IOException
{
ResponseBody body = makeContactDiscoveryRequest(authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
ResponseBody body = makeRequest(ClientSet.ContactDiscovery, authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
if (body != null) {
return JsonUtil.fromJson(body.string(), DiscoveryResponse.class);
@ -629,6 +653,18 @@ public class PushServiceSocket {
}
}
public KeyBackupResponse putKbsData(String authorizationToken, KeyBackupRequest request, List<String> cookies, String mrenclave)
throws IOException
{
ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, cookies, "/v1/backup/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
if (body != null) {
return JsonUtil.fromJson(body.string(), KeyBackupResponse.class);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
}
public void reportContactDiscoveryServiceMatch() throws IOException {
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "ok"), "PUT", "");
}
@ -922,7 +958,12 @@ public class PushServiceSocket {
throw new PushNetworkException(e);
}
throw new LockedException(accountLockFailure.length, accountLockFailure.timeRemaining);
AuthCredentials credentials = accountLockFailure.backupCredentials;
String basicStorageCredentials = credentials != null ? credentials.asBasic() : null;
throw new LockedException(accountLockFailure.length,
accountLockFailure.timeRemaining,
basicStorageCredentials);
}
if (responseCode != 200 && responseCode != 204) {
@ -960,10 +1001,12 @@ public class PushServiceSocket {
request.addHeader(header.getKey(), header.getValue());
}
if (unidentifiedAccess.isPresent()) {
request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
} else if (credentialsProvider.getPassword() != null) {
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
if (!headers.containsKey("Authorization")) {
if (unidentifiedAccess.isPresent()) {
request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
} else if (credentialsProvider.getPassword() != null) {
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
}
}
if (userAgent != null) {
@ -992,15 +1035,33 @@ public class PushServiceSocket {
}
}
private Response makeContactDiscoveryRequest(String authorization, List<String> cookies, String path, String method, String body)
private ConnectionHolder[] clientsFor(ClientSet clientSet) {
switch (clientSet) {
case ContactDiscovery:
return contactDiscoveryClients;
case KeyBackup:
return keyBackupServiceClients;
default:
throw new AssertionError("Unknown attestation purpose");
}
}
Response makeRequest(ClientSet clientSet, String authorization, List<String> cookies, String path, String method, String body)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
ConnectionHolder connectionHolder = getRandom(contactDiscoveryClients, random);
OkHttpClient okHttpClient = connectionHolder.getClient()
.newBuilder()
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
ConnectionHolder connectionHolder = getRandom(clientsFor(clientSet), random);
return makeRequest(connectionHolder, authorization, cookies, path, method, body);
}
private Response makeRequest(ConnectionHolder connectionHolder, String authorization, List<String> cookies, String path, String method, String body)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
OkHttpClient okHttpClient = connectionHolder.getClient()
.newBuilder()
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
@ -1153,12 +1214,26 @@ public class PushServiceSocket {
}
}
private static class RegistrationLockV2 {
@JsonProperty
private String registrationLock;
public RegistrationLockV2() {}
public RegistrationLockV2(String registrationLock) {
this.registrationLock = registrationLock;
}
}
private static class RegistrationLockFailure {
@JsonProperty
private int length;
@JsonProperty
private long timeRemaining;
@JsonProperty
private AuthCredentials backupCredentials;
}
private static class AttachmentDescriptor {
@ -1225,4 +1300,6 @@ public class PushServiceSocket {
@Override
public void handle(int responseCode) { }
}
public enum ClientSet { ContactDiscovery, KeyBackup }
}

View File

@ -0,0 +1,79 @@
package org.whispersystems.signalservice.internal.push;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.IOException;
import java.security.KeyStore;
import java.security.SignatureException;
import java.util.LinkedList;
import java.util.List;
import okhttp3.Response;
import okhttp3.ResponseBody;
public final class RemoteAttestationUtil {
private RemoteAttestationUtil() {
}
public static RemoteAttestation getAndVerifyRemoteAttestation(PushServiceSocket socket,
PushServiceSocket.ClientSet clientSet,
KeyStore iasKeyStore,
String enclaveName,
String mrenclave,
String authorization)
throws IOException, Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException
{
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
Curve25519KeyPair keyPair = curve.generateKeyPair();
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
Pair<RemoteAttestationResponse, List<String>> attestationResponsePair = getRemoteAttestation(socket, clientSet, authorization, attestationRequest, enclaveName);
RemoteAttestationResponse attestationResponse = attestationResponsePair.first();
List<String> attestationCookies = attestationResponsePair.second();
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.getServerEphemeralPublic(), attestationResponse.getServerStaticPublic());
Quote quote = new Quote(attestationResponse.getQuote());
byte[] requestId = RemoteAttestationCipher.getRequestId(keys, attestationResponse);
RemoteAttestationCipher.verifyServerQuote(quote, attestationResponse.getServerStaticPublic(), mrenclave);
RemoteAttestationCipher.verifyIasSignature(iasKeyStore, attestationResponse.getCertificates(), attestationResponse.getSignatureBody(), attestationResponse.getSignature(), quote);
return new RemoteAttestation(requestId, keys, attestationCookies);
}
private static Pair<RemoteAttestationResponse, List<String>> getRemoteAttestation(PushServiceSocket socket,
PushServiceSocket.ClientSet clientSet,
String authorization,
RemoteAttestationRequest request,
String enclaveName)
throws IOException
{
Response response = socket.makeRequest(clientSet, authorization, new LinkedList<String>(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(request));
ResponseBody body = response.body();
List<String> rawCookies = response.headers("Set-Cookie");
List<String> cookies = new LinkedList<>();
for (String cookie : rawCookies) {
cookies.add(cookie.split(";")[0]);
}
if (body != null) {
return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
}
}

View File

@ -0,0 +1,8 @@
package org.whispersystems.signalservice.internal.registrationpin;
public final class InvalidPinException extends Exception {
InvalidPinException(String message) {
super(message);
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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]);
}
}

View File

@ -0,0 +1,127 @@
package org.whispersystems.signalservice.internal.registrationpin;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.whispersystems.signalservice.internal.util.Hex;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public final class PinStretchTest {
private final String pin;
private final byte[] expectedStretchedPin;
private final byte[] expectedKeyPin1;
private final byte[] pinKey2;
private final byte[] expectedMasterKey;
private final String expectedRegistrationLock;
private final byte[] expectedKbsAccessKey;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[]{
"12345",
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
},
new Object[]{
"12345",
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
"abababababababababababababababababababababababababababababababab",
"01198dc427cbf9c6b47f344654d75a263e53b992db73be44b201f357d072dc38",
"bd1f4e129cc705c26c2fcebd3fbc6e7db60caade89e6c465c68ed60aeedbb0c3",
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
},
new Object[]{
"١٢٣٤٥",
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
},
new Object[]{
"9876543210",
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
"abababababababababababababababababababababababababababababababab",
"127a435c15be2528f4b735423f8ee558b789e8ea1f6fe64d144d5b21a87c4e06",
"348d327acb823b54a988cf6bea647a154e21da25cbb121a115c13b871dccd548",
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
},
new Object[]{
"9876543210",
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"128833dbde1af3da703852b6b5a845e226fe9c7e069427b9c1e41279c0cdfb3a",
"6be0b17899cfb5c4316b92acc7db3b6a2fa5b9a19ef3e58a1c84a4de49230aa6",
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
},
new Object[]{
"0123",
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
"abababababababababababababababababababababababababababababababab",
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
},
new Object[]{
"௦௧௨௩",
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
"abababababababababababababababababababababababababababababababab",
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
});
}
public PinStretchTest(String pin,
String expectedStretchedPin,
String expectedKeyPin1,
String pinKey2,
String expectedMasterKey,
String expectedRegistrationLock,
String expectedKbsAccessKey) throws IOException {
this.pin = pin;
this.expectedStretchedPin = Hex.fromStringCondensed(expectedStretchedPin);
this.expectedKeyPin1 = Hex.fromStringCondensed(expectedKeyPin1);
this.pinKey2 = Hex.fromStringCondensed(pinKey2);
this.expectedMasterKey = Hex.fromStringCondensed(expectedMasterKey);
this.expectedRegistrationLock = expectedRegistrationLock;
this.expectedKbsAccessKey = Hex.fromStringCondensed(expectedKbsAccessKey);
}
@Test
public void stretch_pin() throws InvalidPinException {
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
assertArrayEquals(expectedStretchedPin, stretchedPin.getStretchedPin());
assertArrayEquals(expectedKeyPin1, stretchedPin.getPinKey1());
assertArrayEquals(expectedKbsAccessKey, stretchedPin.getKbsAccessKey());
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(pinKey2);
assertArrayEquals(pinKey2, masterKey.getPinKey2());
assertArrayEquals(expectedMasterKey, masterKey.getMasterKey());
assertEquals(expectedRegistrationLock, masterKey.getRegistrationLock());
assertArrayEquals(expectedStretchedPin, masterKey.getStretchedPin());
assertArrayEquals(expectedKeyPin1, masterKey.getPinKey1());
assertArrayEquals(expectedKbsAccessKey, masterKey.getKbsAccessKey());
}
}

View File

@ -687,6 +687,8 @@
<string name="RegistrationActivity_failed_to_verify_the_captcha">Failed to verify the CAPTCHA</string>
<string name="RegistrationActivity_next">Next</string>
<string name="RegistrationActivity_continue">Continue</string>
<string name="RegistrationActivity_continue_d_attempts_left">Continue (%d attempts left)</string>
<string name="RegistrationActivity_continue_last_attempt">Continue (last attempt!)</string>
<string name="RegistrationActivity_take_privacy_with_you_be_yourself_in_every_message">Take privacy with you.\nBe yourself in every message.</string>
<string name="RegistrationActivity_enter_your_phone_number_to_get_started">Enter your phone number to get started</string>
<string name="RegistrationActivity_you_will_receive_a_verification_code">You will receive a verification code. Carrier rates may apply.</string>
@ -1695,6 +1697,8 @@
<string name="RegistrationLockDialog_error_connecting_to_the_service">Error connecting to the service</string>
<string name="RegistrationLockDialog_disable_registration_lock_pin">Disable Registration Lock PIN?</string>
<string name="RegistrationLockDialog_disable">Disable</string>
<string name="RegistrationActivity_pin_incorrect">PIN Incorrect</string>
<string name="RegistrationActivity_you_have_d_tries_remaining">You have %d tries remaining</string>
<string name="preferences_chats__backups">Backups</string>
<string name="prompt_passphrase_activity__signal_is_locked">Signal is locked</string>
<string name="prompt_passphrase_activity__tap_to_unlock">TAP TO UNLOCK</string>

View File

@ -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();

View File

@ -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());

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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<RegistrationPinV2MigrationJob> {
@Override
public @NonNull RegistrationPinV2MigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new RegistrationPinV2MigrationJob(parameters);
}
}
}

View File

@ -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);

View File

@ -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<String, SignalServiceConfiguration>() {{
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]);
}

View File

@ -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<Boolean>() {
@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<Boolean>() {

View File

@ -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);

View File

@ -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<Void, Void, Result>() {
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<PreKeyRecord> 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();

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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<NumberViewState> number;
private final MutableLiveData<String> textCodeEntered;
private final MutableLiveData<String> captchaToken;
private final MutableLiveData<String> fcmToken;
private final MutableLiveData<String> basicStorageCredentials;
private final MutableLiveData<Boolean> restoreFlowShown;
private final MutableLiveData<Integer> successfulCodeRequestAttempts;
private final MutableLiveData<LocalCodeRequestRateLimiter> requestLimiter;
private final MutableLiveData<String> keyBackupcurrentTokenJson;
private final LiveData<TokenResponse> keyBackupcurrentToken;
private final LiveData<Pair<TokenResponse, String>> 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> 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<Pair<TokenResponse, String>> 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<A, B> extends MediatorLiveData<Pair<A, B>> {
private A a;
private B b;
public LiveDataPair(LiveData<A> ld1, LiveData<B> 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));
});
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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);
}