Add internal pre-alpha support for Registration Lock v2.
parent
058c25808b
commit
7f8ca58762
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Copyright (C) 2019 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
syntax = "proto2";
|
||||
|
||||
package textsecure;
|
||||
|
||||
option java_package = "org.whispersystems.signalservice.internal.keybackup.protos";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Request {
|
||||
optional BackupRequest backup = 1;
|
||||
optional RestoreRequest restore = 2;
|
||||
optional DeleteRequest delete = 3;
|
||||
}
|
||||
|
||||
message Response {
|
||||
optional BackupResponse backup = 1;
|
||||
optional RestoreResponse restore = 2;
|
||||
optional DeleteResponse delete = 3;
|
||||
}
|
||||
|
||||
message BackupRequest {
|
||||
optional bytes service_id = 1;
|
||||
optional bytes backup_id = 2;
|
||||
optional bytes token = 3;
|
||||
optional uint64 valid_from = 4;
|
||||
optional bytes data = 5;
|
||||
optional bytes pin = 6;
|
||||
optional uint32 tries = 7;
|
||||
}
|
||||
|
||||
message BackupResponse {
|
||||
enum Status {
|
||||
OK = 1;
|
||||
ALREADY_EXISTS = 2;
|
||||
NOT_YET_VALID = 3;
|
||||
}
|
||||
|
||||
optional Status status = 1;
|
||||
optional bytes token = 2;
|
||||
}
|
||||
|
||||
message RestoreRequest {
|
||||
optional bytes service_id = 1;
|
||||
optional bytes backup_id = 2;
|
||||
optional bytes token = 3;
|
||||
optional uint64 valid_from = 4;
|
||||
optional bytes pin = 5;
|
||||
}
|
||||
|
||||
message RestoreResponse {
|
||||
enum Status {
|
||||
OK = 1;
|
||||
TOKEN_MISMATCH = 2;
|
||||
NOT_YET_VALID = 3;
|
||||
MISSING = 4;
|
||||
PIN_MISMATCH = 5;
|
||||
}
|
||||
|
||||
optional Status status = 1;
|
||||
optional bytes token = 2;
|
||||
optional bytes data = 3;
|
||||
optional uint32 tries = 4;
|
||||
}
|
||||
|
||||
message DeleteRequest {
|
||||
optional bytes service_id = 1;
|
||||
optional bytes backup_id = 2;
|
||||
}
|
||||
|
||||
message DeleteResponse {
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class KeyBackupService {
|
||||
|
||||
private static final String TAG = KeyBackupService.class.getSimpleName();
|
||||
|
||||
private final KeyStore iasKeyStore;
|
||||
private final String enclaveName;
|
||||
private final String mrenclave;
|
||||
private final PushServiceSocket pushServiceSocket;
|
||||
private final int maxTries;
|
||||
|
||||
KeyBackupService(KeyStore iasKeyStore,
|
||||
String enclaveName,
|
||||
String mrenclave,
|
||||
PushServiceSocket pushServiceSocket,
|
||||
int maxTries)
|
||||
{
|
||||
this.iasKeyStore = iasKeyStore;
|
||||
this.enclaveName = enclaveName;
|
||||
this.mrenclave = mrenclave;
|
||||
this.pushServiceSocket = pushServiceSocket;
|
||||
this.maxTries = maxTries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this if you don't want to validate that the server has not changed since you last set the pin.
|
||||
*/
|
||||
public PinChangeSession newPinChangeSession()
|
||||
throws IOException
|
||||
{
|
||||
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this if you want to validate that the server has not changed since you last set the pin.
|
||||
* The supplied token will have to match for the change to be successful.
|
||||
*/
|
||||
public PinChangeSession newPinChangeSession(TokenResponse currentToken)
|
||||
throws IOException
|
||||
{
|
||||
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to validate that the pin is still set on the server with the current token.
|
||||
* Additionally this validates that no one has used any tries.
|
||||
*/
|
||||
public RestoreSession newRestoreSession(TokenResponse currentToken)
|
||||
throws IOException
|
||||
{
|
||||
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization(), currentToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only call before registration, to see how many tries are left.
|
||||
* <p>
|
||||
* Pass the token to the newRegistrationSession.
|
||||
*/
|
||||
public TokenResponse getToken(String authAuthorization) throws IOException {
|
||||
return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt.
|
||||
*
|
||||
* @param tokenResponse Supplying a token response from a failed previous attempt prevents certain attacks.
|
||||
*/
|
||||
public RestoreSession newRegistrationSession(String authAuthorization, TokenResponse tokenResponse)
|
||||
throws IOException
|
||||
{
|
||||
return newSession(authAuthorization, tokenResponse);
|
||||
}
|
||||
|
||||
private Session newSession(String authorization, TokenResponse currentToken)
|
||||
throws IOException
|
||||
{
|
||||
TokenResponse token = currentToken != null ? currentToken : pushServiceSocket.getKeyBackupServiceToken(authorization, enclaveName);
|
||||
|
||||
return new Session(authorization, token);
|
||||
}
|
||||
|
||||
private class Session implements RestoreSession, PinChangeSession {
|
||||
|
||||
private final String authorization;
|
||||
private final TokenResponse currentToken;
|
||||
|
||||
Session(String authorization, TokenResponse currentToken) {
|
||||
this.authorization = authorization;
|
||||
this.currentToken = currentToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistrationLockData restorePin(String pin)
|
||||
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException
|
||||
{
|
||||
int attempt = 0;
|
||||
SecureRandom random = new SecureRandom();
|
||||
TokenResponse token = currentToken;
|
||||
|
||||
while (true) {
|
||||
|
||||
attempt++;
|
||||
|
||||
try {
|
||||
return restorePin(pin, token);
|
||||
} catch (TokenException tokenException) {
|
||||
|
||||
token = tokenException.getToken();
|
||||
|
||||
if (tokenException instanceof KeyBackupServicePinException) {
|
||||
throw (KeyBackupServicePinException) tokenException;
|
||||
}
|
||||
|
||||
if (tokenException.isCanAutomaticallyRetry() && attempt < 5) {
|
||||
// back off randomly, between 250 and 8000 ms
|
||||
int backoffMs = 250 * (1 << (attempt - 1));
|
||||
|
||||
Util.sleep(backoffMs + random.nextInt(backoffMs));
|
||||
} else {
|
||||
throw new UnauthenticatedResponseException("Token mismatch, expended all automatic retries");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RegistrationLockData restorePin(String pin, TokenResponse token)
|
||||
throws UnauthenticatedResponseException, IOException, TokenException, InvalidPinException
|
||||
{
|
||||
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
|
||||
|
||||
try {
|
||||
final int remainingTries = token.getTries();
|
||||
final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||
final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(stretchedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName));
|
||||
final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||
final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation);
|
||||
|
||||
TokenResponse nextToken = status.hasToken()
|
||||
? new TokenResponse(token.getBackupId(), status.getToken().toByteArray(), status.getTries())
|
||||
: token;
|
||||
|
||||
Log.i(TAG, "Restore " + status.getStatus());
|
||||
switch (status.getStatus()) {
|
||||
case OK:
|
||||
Log.i(TAG, String.format(Locale.US,"Restore OK! data: %s tries: %d", Hex.toStringCondensed(status.getData().toByteArray()), status.getTries()));
|
||||
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(status.getData().toByteArray());
|
||||
return new RegistrationLockData(masterKey, nextToken);
|
||||
case PIN_MISMATCH:
|
||||
Log.i(TAG, "Restore PIN_MISMATCH");
|
||||
throw new KeyBackupServicePinException(nextToken);
|
||||
case TOKEN_MISMATCH:
|
||||
Log.i(TAG, "Restore TOKEN_MISMATCH");
|
||||
// if the number of tries has not fallen, the pin is correct we're just using an out of date token
|
||||
boolean canRetry = remainingTries == status.getTries();
|
||||
Log.i(TAG, String.format(Locale.US, "Token MISMATCH %d %d", remainingTries, status.getTries()));
|
||||
Log.i(TAG, String.format("Last token %s", Hex.toStringCondensed(token.getToken())));
|
||||
Log.i(TAG, String.format("Next token %s", Hex.toStringCondensed(nextToken.getToken())));
|
||||
throw new TokenException(nextToken, canRetry);
|
||||
case MISSING:
|
||||
Log.i(TAG, "Restore OK! No data though");
|
||||
return null;
|
||||
case NOT_YET_VALID:
|
||||
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
|
||||
}
|
||||
} catch (InvalidCiphertextException e) {
|
||||
throw new UnauthenticatedResponseException(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private RemoteAttestation getAndVerifyRemoteAttestation() throws UnauthenticatedResponseException, IOException {
|
||||
try {
|
||||
return RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.KeyBackup, iasKeyStore, enclaveName, mrenclave, authorization);
|
||||
} catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | InvalidCiphertextException | SignatureException e) {
|
||||
throw new UnauthenticatedResponseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistrationLockData setPin(String pin) throws IOException, UnauthenticatedResponseException, InvalidPinException {
|
||||
PinStretcher.MasterKey masterKey = PinStretcher.stretchPin(pin)
|
||||
.withNewSecurePinKey2();
|
||||
|
||||
TokenResponse tokenResponse = putKbsData(masterKey.getKbsAccessKey(),
|
||||
masterKey.getPinKey2(),
|
||||
enclaveName,
|
||||
currentToken);
|
||||
|
||||
pushServiceSocket.setRegistrationLock(masterKey.getRegistrationLock());
|
||||
|
||||
return new RegistrationLockData(masterKey, tokenResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePin() throws IOException, UnauthenticatedResponseException {
|
||||
deleteKbsData();
|
||||
|
||||
pushServiceSocket.removePinV2();
|
||||
}
|
||||
|
||||
private TokenResponse putKbsData(byte[] kbsAccessKey, byte[] kbsData, String enclaveName, TokenResponse token)
|
||||
throws IOException, UnauthenticatedResponseException
|
||||
{
|
||||
try {
|
||||
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||
KeyBackupRequest request = KeyBackupCipher.createKeyBackupRequest(kbsAccessKey, kbsData, token, remoteAttestation, Hex.fromStringCondensed(enclaveName), maxTries);
|
||||
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||
BackupResponse backupResponse = KeyBackupCipher.getKeyBackupResponse(response, remoteAttestation);
|
||||
BackupResponse.Status status = backupResponse.getStatus();
|
||||
|
||||
switch (status) {
|
||||
case OK:
|
||||
return backupResponse.hasToken() ? new TokenResponse(token.getBackupId(), backupResponse.getToken().toByteArray(), maxTries) : token;
|
||||
case ALREADY_EXISTS:
|
||||
throw new UnauthenticatedResponseException("Already exists");
|
||||
case NOT_YET_VALID:
|
||||
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
|
||||
default:
|
||||
throw new AssertionError("Unknown response status " + status);
|
||||
}
|
||||
} catch (InvalidCiphertextException e) {
|
||||
throw new UnauthenticatedResponseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteKbsData()
|
||||
throws IOException, UnauthenticatedResponseException
|
||||
{
|
||||
try {
|
||||
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||
KeyBackupRequest request = KeyBackupCipher.createKeyDeleteRequest(currentToken, remoteAttestation, Hex.fromStringCondensed(enclaveName));
|
||||
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||
|
||||
KeyBackupCipher.getKeyDeleteResponseStatus(response, remoteAttestation);
|
||||
} catch (InvalidCiphertextException e) {
|
||||
throw new UnauthenticatedResponseException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface RestoreSession {
|
||||
|
||||
RegistrationLockData restorePin(String pin)
|
||||
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException;
|
||||
}
|
||||
|
||||
public interface PinChangeSession {
|
||||
|
||||
RegistrationLockData setPin(String pin)
|
||||
throws IOException, UnauthenticatedResponseException, InvalidPinException;
|
||||
|
||||
void removePin()
|
||||
throws IOException, UnauthenticatedResponseException;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
public final class KeyBackupServicePinException extends TokenException {
|
||||
|
||||
private final int triesRemaining;
|
||||
|
||||
public KeyBackupServicePinException(TokenResponse nextToken) {
|
||||
super(nextToken, false);
|
||||
this.triesRemaining = nextToken.getTries();
|
||||
}
|
||||
|
||||
public int getTriesRemaining() {
|
||||
return triesRemaining;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
||||
|
||||
public final class RegistrationLockData {
|
||||
|
||||
private final PinStretcher.MasterKey masterKey;
|
||||
private final TokenResponse tokenResponse;
|
||||
|
||||
RegistrationLockData(PinStretcher.MasterKey masterKey, TokenResponse tokenResponse) {
|
||||
this.masterKey = masterKey;
|
||||
this.tokenResponse = tokenResponse;
|
||||
}
|
||||
|
||||
public PinStretcher.MasterKey getMasterKey() {
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
public TokenResponse getTokenResponse() {
|
||||
return tokenResponse;
|
||||
}
|
||||
|
||||
public int getRemainingTries() {
|
||||
return tokenResponse.getTries();
|
||||
}
|
||||
}
|
|
@ -9,8 +9,6 @@ package org.whispersystems.signalservice.api;
|
|||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.whispersystems.curve25519.Curve25519;
|
||||
import org.whispersystems.curve25519.Curve25519KeyPair;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
|
@ -18,7 +16,6 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
|
|||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
|
@ -33,23 +30,19 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
|
|||
import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
|
||||
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
|
||||
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
|
||||
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
|
@ -117,18 +110,34 @@ public class SignalServiceAccountManager {
|
|||
return this.pushServiceSocket.getSenderCertificateLegacy();
|
||||
}
|
||||
|
||||
public void setPin(Optional<String> pin) throws IOException {
|
||||
if (pin.isPresent()) {
|
||||
this.pushServiceSocket.setPin(pin.get());
|
||||
} else {
|
||||
this.pushServiceSocket.removePin();
|
||||
}
|
||||
/**
|
||||
* @deprecated Remove this method once KBS is live.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setPin(String pin) throws IOException {
|
||||
this.pushServiceSocket.setPin(pin);
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 Pin setting has been replaced by KeyBackupService.
|
||||
* Now you can only remove the old pin but there is no need to remove the old pin if setting a KBS Pin.
|
||||
*/
|
||||
public void removeV1Pin() throws IOException {
|
||||
this.pushServiceSocket.removePin();
|
||||
}
|
||||
|
||||
public UUID getOwnUuid() throws IOException {
|
||||
return this.pushServiceSocket.getOwnUuid();
|
||||
}
|
||||
|
||||
public KeyBackupService getKeyBackupService(KeyStore iasKeyStore,
|
||||
String enclaveName,
|
||||
String mrenclave,
|
||||
int tries)
|
||||
{
|
||||
return new KeyBackupService(iasKeyStore, enclaveName, mrenclave, pushServiceSocket, tries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register/Unregister a Google Cloud Messaging registration ID.
|
||||
*
|
||||
|
@ -193,16 +202,20 @@ public class SignalServiceAccountManager {
|
|||
* This value should remain consistent across registrations for the
|
||||
* same install, but probabilistically differ across registrations
|
||||
* for separate installs.
|
||||
* @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS.
|
||||
* @param registrationLock Only supply if found on KBS.
|
||||
* @return The UUID of the user that was registered.
|
||||
* @throws IOException
|
||||
*/
|
||||
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
|
||||
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
|
||||
String pin, String registrationLock,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
|
||||
throws IOException
|
||||
{
|
||||
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
|
||||
signalProtocolRegistrationId,
|
||||
fetchesMessages, pin,
|
||||
fetchesMessages,
|
||||
pin, registrationLock,
|
||||
unidentifiedAccessKey,
|
||||
unrestrictedUnidentifiedAccess);
|
||||
}
|
||||
|
@ -215,14 +228,18 @@ public class SignalServiceAccountManager {
|
|||
* This value should remain consistent across registrations for the same
|
||||
* install, but probabilistically differ across registrations for
|
||||
* separate installs.
|
||||
* @param pin Only supply if pin has not yet been migrated to KBS.
|
||||
* @param registrationLock Only supply if found on KBS.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin,
|
||||
public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
|
||||
String pin, String registrationLock,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
|
||||
throws IOException
|
||||
{
|
||||
this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages, pin,
|
||||
this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages,
|
||||
pin, registrationLock,
|
||||
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
|
||||
}
|
||||
|
||||
|
@ -306,35 +323,21 @@ public class SignalServiceAccountManager {
|
|||
return activeTokens;
|
||||
}
|
||||
|
||||
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String mrenclave)
|
||||
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String enclaveId)
|
||||
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
|
||||
{
|
||||
try {
|
||||
String authorization = this.pushServiceSocket.getContactDiscoveryAuthorization();
|
||||
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
|
||||
Curve25519KeyPair keyPair = curve.generateKeyPair();
|
||||
|
||||
ContactDiscoveryCipher cipher = new ContactDiscoveryCipher();
|
||||
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
|
||||
Pair<RemoteAttestationResponse, List<String>> attestationResponse = this.pushServiceSocket.getContactDiscoveryRemoteAttestation(authorization, attestationRequest, mrenclave);
|
||||
|
||||
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.first().getServerEphemeralPublic(), attestationResponse.first().getServerStaticPublic());
|
||||
Quote quote = new Quote(attestationResponse.first().getQuote());
|
||||
byte[] requestId = cipher.getRequestId(keys, attestationResponse.first());
|
||||
|
||||
cipher.verifyServerQuote(quote, attestationResponse.first().getServerStaticPublic(), mrenclave);
|
||||
cipher.verifyIasSignature(iasKeyStore, attestationResponse.first().getCertificates(), attestationResponse.first().getSignatureBody(), attestationResponse.first().getSignature(), quote);
|
||||
|
||||
RemoteAttestation remoteAttestation = new RemoteAttestation(requestId, keys);
|
||||
List<String> addressBook = new LinkedList<>();
|
||||
String authorization = pushServiceSocket.getContactDiscoveryAuthorization();
|
||||
RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization);
|
||||
List<String> addressBook = new LinkedList<>();
|
||||
|
||||
for (String e164number : e164numbers) {
|
||||
addressBook.add(e164number.substring(1));
|
||||
}
|
||||
|
||||
DiscoveryRequest request = cipher.createDiscoveryRequest(addressBook, remoteAttestation);
|
||||
DiscoveryResponse response = this.pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, attestationResponse.second(), mrenclave);
|
||||
byte[] data = cipher.getDiscoveryResponseData(response, remoteAttestation);
|
||||
DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, remoteAttestation);
|
||||
DiscoveryResponse response = pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, remoteAttestation.getCookies(), enclaveId);
|
||||
byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, remoteAttestation);
|
||||
|
||||
Iterator<String> addressBookIterator = addressBook.iterator();
|
||||
List<String> results = new LinkedList<>();
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
class TokenException extends Exception {
|
||||
|
||||
private final TokenResponse nextToken;
|
||||
private final boolean canAutomaticallyRetry;
|
||||
|
||||
TokenException(TokenResponse nextToken, boolean canAutomaticallyRetry) {
|
||||
this.nextToken = nextToken;
|
||||
this.canAutomaticallyRetry = canAutomaticallyRetry;
|
||||
}
|
||||
|
||||
public TokenResponse getToken() {
|
||||
return nextToken;
|
||||
}
|
||||
|
||||
public boolean isCanAutomaticallyRetry() {
|
||||
return canAutomaticallyRetry;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.whispersystems.signalservice.internal.configuration;
|
||||
|
||||
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
|
||||
import okhttp3.ConnectionSpec;
|
||||
|
||||
public class SignalKeyBackupServiceUrl extends SignalUrl {
|
||||
|
||||
public SignalKeyBackupServiceUrl(String url, TrustStore trustStore) {
|
||||
super(url, trustStore);
|
||||
}
|
||||
|
||||
public SignalKeyBackupServiceUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) {
|
||||
super(url, hostHeader, trustStore, connectionSpec);
|
||||
}
|
||||
}
|
|
@ -6,11 +6,16 @@ public class SignalServiceConfiguration {
|
|||
private final SignalServiceUrl[] signalServiceUrls;
|
||||
private final SignalCdnUrl[] signalCdnUrls;
|
||||
private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls;
|
||||
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
|
||||
|
||||
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, SignalCdnUrl[] signalCdnUrls, SignalContactDiscoveryUrl[] signalContactDiscoveryUrls) {
|
||||
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
|
||||
SignalCdnUrl[] signalCdnUrls,
|
||||
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
|
||||
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls) {
|
||||
this.signalServiceUrls = signalServiceUrls;
|
||||
this.signalCdnUrls = signalCdnUrls;
|
||||
this.signalContactDiscoveryUrls = signalContactDiscoveryUrls;
|
||||
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
|
||||
}
|
||||
|
||||
public SignalServiceUrl[] getSignalServiceUrls() {
|
||||
|
@ -24,4 +29,8 @@ public class SignalServiceConfiguration {
|
|||
public SignalContactDiscoveryUrl[] getSignalContactDiscoveryUrls() {
|
||||
return signalContactDiscoveryUrls;
|
||||
}
|
||||
|
||||
public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() {
|
||||
return signalKeyBackupServiceUrls;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package org.whispersystems.signalservice.internal.contacts.crypto;
|
||||
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
final class AESCipher {
|
||||
|
||||
private static final int TAG_LENGTH_BYTES = 16;
|
||||
private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8;
|
||||
|
||||
static byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, iv));
|
||||
|
||||
return cipher.doFinal(ByteUtil.combine(ciphertext, tag));
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException | BadPaddingException e) {
|
||||
throw new InvalidCiphertextException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static AESEncryptedResult encrypt(byte[] key, byte[] aad, byte[] requestData) {
|
||||
try {
|
||||
byte[] iv = Util.getSecretBytes(12);
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, iv));
|
||||
cipher.updateAAD(aad);
|
||||
|
||||
byte[] cipherText = cipher.doFinal(requestData);
|
||||
byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);
|
||||
|
||||
byte[] mac = parts[1];
|
||||
byte[] data = parts[0];
|
||||
|
||||
return new AESEncryptedResult(iv, data, mac, aad);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
static class AESEncryptedResult {
|
||||
final byte[] iv;
|
||||
final byte[] data;
|
||||
final byte[] mac;
|
||||
final byte[] aad;
|
||||
|
||||
private AESEncryptedResult(byte[] iv, byte[] data, byte[] mac, byte[] aad) {
|
||||
this.iv = iv;
|
||||
this.data = data;
|
||||
this.mac = mac;
|
||||
this.aad = aad;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +1,20 @@
|
|||
package org.whispersystems.signalservice.internal.contacts.crypto;
|
||||
|
||||
|
||||
import org.threeten.bp.Instant;
|
||||
import org.threeten.bp.LocalDateTime;
|
||||
import org.threeten.bp.Period;
|
||||
import org.threeten.bp.ZoneId;
|
||||
import org.threeten.bp.ZonedDateTime;
|
||||
import org.threeten.bp.format.DateTimeFormatter;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
public final class ContactDiscoveryCipher {
|
||||
|
||||
public class ContactDiscoveryCipher {
|
||||
private ContactDiscoveryCipher() {
|
||||
}
|
||||
|
||||
private static final int TAG_LENGTH_BYTES = 16;
|
||||
private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8;
|
||||
private static final long SIGNATURE_BODY_VERSION = 3L;
|
||||
|
||||
public DiscoveryRequest createDiscoveryRequest(List<String> addressBook, RemoteAttestation remoteAttestation) {
|
||||
public static DiscoveryRequest createDiscoveryRequest(List<String> addressBook, RemoteAttestation remoteAttestation) {
|
||||
try {
|
||||
ByteArrayOutputStream requestDataStream = new ByteArrayOutputStream();
|
||||
|
||||
|
@ -49,100 +22,19 @@ public class ContactDiscoveryCipher {
|
|||
requestDataStream.write(ByteUtil.longToByteArray(Long.parseLong(address)));
|
||||
}
|
||||
|
||||
byte[] requestData = requestDataStream.toByteArray();
|
||||
byte[] nonce = Util.getSecretBytes(12);
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
byte[] clientKey = remoteAttestation.getKeys().getClientKey();
|
||||
byte[] requestData = requestDataStream.toByteArray();
|
||||
byte[] aad = remoteAttestation.getRequestId();
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(remoteAttestation.getKeys().getClientKey(), "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, nonce));
|
||||
cipher.updateAAD(remoteAttestation.getRequestId());
|
||||
AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData);
|
||||
|
||||
byte[] cipherText = cipher.doFinal(requestData);
|
||||
byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);
|
||||
|
||||
return new DiscoveryRequest(addressBook.size(), remoteAttestation.getRequestId(), nonce, parts[0], parts[1]);
|
||||
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
|
||||
return decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
|
||||
}
|
||||
|
||||
public byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException {
|
||||
return decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag());
|
||||
}
|
||||
|
||||
public void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave)
|
||||
throws UnauthenticatedQuoteException
|
||||
{
|
||||
try {
|
||||
byte[] theirServerPublicStatic = new byte[serverPublicStatic.length];
|
||||
System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length);
|
||||
|
||||
if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) {
|
||||
throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!");
|
||||
}
|
||||
|
||||
if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) {
|
||||
throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave()));
|
||||
}
|
||||
|
||||
if (quote.isDebugQuote()) {
|
||||
throw new UnauthenticatedQuoteException("Received quote for debuggable enclave");
|
||||
}
|
||||
return new DiscoveryRequest(addressBook.size(), aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac);
|
||||
} catch (IOException e) {
|
||||
throw new UnauthenticatedQuoteException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote)
|
||||
throws SignatureException
|
||||
{
|
||||
if (certificates == null || certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates.");
|
||||
}
|
||||
|
||||
try {
|
||||
SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore);
|
||||
signingCertificate.verifySignature(signatureBody, signature);
|
||||
|
||||
SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class);
|
||||
|
||||
if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) {
|
||||
throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion());
|
||||
}
|
||||
|
||||
if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) {
|
||||
throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes()));
|
||||
}
|
||||
|
||||
if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) {
|
||||
throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus());
|
||||
}
|
||||
|
||||
if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC")))
|
||||
.plus(Period.ofDays(1))
|
||||
.isBefore(Instant.now()))
|
||||
{
|
||||
throw new SignatureException("Signature is expired");
|
||||
}
|
||||
|
||||
} catch (CertificateException | CertPathValidatorException | IOException e) {
|
||||
throw new SignatureException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decrypt(byte[] key, byte[] iv, byte[] ciphertext, byte[] tag) throws InvalidCiphertextException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
|
||||
|
||||
return cipher.doFinal(ByteUtil.combine(ciphertext, tag));
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException | BadPaddingException e) {
|
||||
throw new InvalidCiphertextException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
|
||||
return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
package org.whispersystems.signalservice.internal.contacts.crypto;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.BackupRequest;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.DeleteRequest;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.DeleteResponse;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.Request;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.Response;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreRequest;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class KeyBackupCipher {
|
||||
|
||||
private KeyBackupCipher() {
|
||||
}
|
||||
|
||||
private static final long VALID_FROM_BUFFER_MS = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
public static KeyBackupRequest createKeyBackupRequest(byte[] kbsAccessKey,
|
||||
byte[] kbsData,
|
||||
TokenResponse token,
|
||||
RemoteAttestation remoteAttestation,
|
||||
byte[] serviceId,
|
||||
int tries)
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
BackupRequest backupRequest = BackupRequest.newBuilder()
|
||||
.setServiceId(ByteString.copyFrom(serviceId))
|
||||
.setBackupId(ByteString.copyFrom(token.getBackupId()))
|
||||
.setToken(ByteString.copyFrom(token.getToken()))
|
||||
.setValidFrom(getValidFromSeconds(now))
|
||||
.setData(ByteString.copyFrom(kbsData))
|
||||
.setPin(ByteString.copyFrom(kbsAccessKey))
|
||||
.setTries(tries)
|
||||
.build();
|
||||
|
||||
Request requestData = Request.newBuilder().setBackup(backupRequest).build();
|
||||
|
||||
return createKeyBackupRequest(requestData, remoteAttestation);
|
||||
}
|
||||
|
||||
public static KeyBackupRequest createKeyRestoreRequest(byte[] kbsAccessKey,
|
||||
TokenResponse token,
|
||||
RemoteAttestation remoteAttestation,
|
||||
byte[] serviceId)
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
RestoreRequest restoreRequest = RestoreRequest.newBuilder()
|
||||
.setServiceId(ByteString.copyFrom(serviceId))
|
||||
.setBackupId(ByteString.copyFrom(token.getBackupId()))
|
||||
.setToken(ByteString.copyFrom(token.getToken()))
|
||||
.setValidFrom(getValidFromSeconds(now))
|
||||
.setPin(ByteString.copyFrom(kbsAccessKey))
|
||||
.build();
|
||||
|
||||
Request request = Request.newBuilder().setRestore(restoreRequest).build();
|
||||
|
||||
return createKeyBackupRequest(request, remoteAttestation);
|
||||
}
|
||||
|
||||
public static KeyBackupRequest createKeyDeleteRequest(TokenResponse token,
|
||||
RemoteAttestation remoteAttestation,
|
||||
byte[] serviceId)
|
||||
{
|
||||
DeleteRequest deleteRequest = DeleteRequest.newBuilder()
|
||||
.setServiceId(ByteString.copyFrom(serviceId))
|
||||
.setBackupId(ByteString.copyFrom(token.getBackupId()))
|
||||
.build();
|
||||
|
||||
Request request = Request.newBuilder().setDelete(deleteRequest).build();
|
||||
|
||||
return createKeyBackupRequest(request, remoteAttestation);
|
||||
}
|
||||
|
||||
public static BackupResponse getKeyBackupResponse(KeyBackupResponse response, RemoteAttestation remoteAttestation)
|
||||
throws InvalidCiphertextException, InvalidProtocolBufferException
|
||||
{
|
||||
byte[] data = decryptData(response, remoteAttestation);
|
||||
|
||||
Response backupResponse = Response.parseFrom(data);
|
||||
|
||||
return backupResponse.getBackup();
|
||||
}
|
||||
|
||||
public static RestoreResponse getKeyRestoreResponse(KeyBackupResponse response, RemoteAttestation remoteAttestation)
|
||||
throws InvalidCiphertextException, InvalidProtocolBufferException
|
||||
{
|
||||
byte[] data = decryptData(response, remoteAttestation);
|
||||
|
||||
return Response.parseFrom(data).getRestore();
|
||||
}
|
||||
|
||||
public static DeleteResponse getKeyDeleteResponseStatus(KeyBackupResponse response, RemoteAttestation remoteAttestation)
|
||||
throws InvalidCiphertextException, InvalidProtocolBufferException
|
||||
{
|
||||
byte[] data = decryptData(response, remoteAttestation);
|
||||
|
||||
return DeleteResponse.parseFrom(data);
|
||||
}
|
||||
|
||||
private static KeyBackupRequest createKeyBackupRequest(Request requestData, RemoteAttestation remoteAttestation) {
|
||||
byte[] clientKey = remoteAttestation.getKeys().getClientKey();
|
||||
byte[] aad = remoteAttestation.getRequestId();
|
||||
|
||||
AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData.toByteArray());
|
||||
|
||||
return new KeyBackupRequest(aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac);
|
||||
}
|
||||
|
||||
private static byte[] decryptData(KeyBackupResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
|
||||
return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
|
||||
}
|
||||
|
||||
private static long getValidFromSeconds(long nowMs) {
|
||||
return TimeUnit.MILLISECONDS.toSeconds(nowMs - VALID_FROM_BUFFER_MS);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
package org.whispersystems.signalservice.internal.contacts.crypto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RemoteAttestation {
|
||||
|
||||
private final byte[] requestId;
|
||||
private final RemoteAttestationKeys keys;
|
||||
private final List<String> cookies;
|
||||
|
||||
public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys) {
|
||||
public RemoteAttestation(byte[] requestId, RemoteAttestationKeys keys, List<String> cookies) {
|
||||
this.requestId = requestId;
|
||||
this.keys = keys;
|
||||
this.cookies = cookies;
|
||||
}
|
||||
|
||||
public byte[] getRequestId() {
|
||||
|
@ -17,4 +21,8 @@ public class RemoteAttestation {
|
|||
public RemoteAttestationKeys getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
public List<String> getCookies() {
|
||||
return cookies;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package org.whispersystems.signalservice.internal.contacts.crypto;
|
||||
|
||||
import org.threeten.bp.Instant;
|
||||
import org.threeten.bp.LocalDateTime;
|
||||
import org.threeten.bp.Period;
|
||||
import org.threeten.bp.ZoneId;
|
||||
import org.threeten.bp.ZonedDateTime;
|
||||
import org.threeten.bp.format.DateTimeFormatter;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
public final class RemoteAttestationCipher {
|
||||
|
||||
private RemoteAttestationCipher() {
|
||||
}
|
||||
|
||||
private static final long SIGNATURE_BODY_VERSION = 3L;
|
||||
|
||||
public static byte[] getRequestId(RemoteAttestationKeys keys, RemoteAttestationResponse response) throws InvalidCiphertextException {
|
||||
return AESCipher.decrypt(keys.getServerKey(), response.getIv(), response.getCiphertext(), response.getTag());
|
||||
}
|
||||
|
||||
public static void verifyServerQuote(Quote quote, byte[] serverPublicStatic, String mrenclave)
|
||||
throws UnauthenticatedQuoteException
|
||||
{
|
||||
try {
|
||||
byte[] theirServerPublicStatic = new byte[serverPublicStatic.length];
|
||||
System.arraycopy(quote.getReportData(), 0, theirServerPublicStatic, 0, theirServerPublicStatic.length);
|
||||
|
||||
if (!MessageDigest.isEqual(theirServerPublicStatic, serverPublicStatic)) {
|
||||
throw new UnauthenticatedQuoteException("Response quote has unauthenticated report data!");
|
||||
}
|
||||
|
||||
if (!MessageDigest.isEqual(Hex.fromStringCondensed(mrenclave), quote.getMrenclave())) {
|
||||
throw new UnauthenticatedQuoteException("The response quote has the wrong mrenclave value in it: " + Hex.toStringCondensed(quote.getMrenclave()));
|
||||
}
|
||||
|
||||
if (quote.isDebugQuote()) {
|
||||
throw new UnauthenticatedQuoteException("Received quote for debuggable enclave");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UnauthenticatedQuoteException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void verifyIasSignature(KeyStore trustStore, String certificates, String signatureBody, String signature, Quote quote)
|
||||
throws SignatureException
|
||||
{
|
||||
if (certificates == null || certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates.");
|
||||
}
|
||||
|
||||
try {
|
||||
SigningCertificate signingCertificate = new SigningCertificate(certificates, trustStore);
|
||||
signingCertificate.verifySignature(signatureBody, signature);
|
||||
|
||||
SignatureBodyEntity signatureBodyEntity = JsonUtil.fromJson(signatureBody, SignatureBodyEntity.class);
|
||||
|
||||
if (signatureBodyEntity.getVersion() != SIGNATURE_BODY_VERSION) {
|
||||
throw new SignatureException("Unexpected signed quote version " + signatureBodyEntity.getVersion());
|
||||
}
|
||||
|
||||
if (!MessageDigest.isEqual(ByteUtil.trim(signatureBodyEntity.getIsvEnclaveQuoteBody(), 432), ByteUtil.trim(quote.getQuoteBytes(), 432))) {
|
||||
throw new SignatureException("Signed quote is not the same as RA quote: " + Hex.toStringCondensed(signatureBodyEntity.getIsvEnclaveQuoteBody()) + " vs " + Hex.toStringCondensed(quote.getQuoteBytes()));
|
||||
}
|
||||
|
||||
if (!"OK".equals(signatureBodyEntity.getIsvEnclaveQuoteStatus())) {
|
||||
throw new SignatureException("Quote status is: " + signatureBodyEntity.getIsvEnclaveQuoteStatus());
|
||||
}
|
||||
|
||||
if (Instant.from(ZonedDateTime.of(LocalDateTime.from(DateTimeFormatter.ofPattern("yyy-MM-dd'T'HH:mm:ss.SSSSSS").parse(signatureBodyEntity.getTimestamp())), ZoneId.of("UTC")))
|
||||
.plus(Period.ofDays(1))
|
||||
.isBefore(Instant.now()))
|
||||
{
|
||||
throw new SignatureException("Signature is expired");
|
||||
}
|
||||
|
||||
} catch (CertificateException | CertPathValidatorException | IOException e) {
|
||||
throw new SignatureException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,11 +31,15 @@ public class SigningCertificate {
|
|||
{
|
||||
try {
|
||||
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||
Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) certificateFactory.generateCertificates(new ByteArrayInputStream(URLDecoder.decode(certificateChain).getBytes()));
|
||||
Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) certificateFactory.generateCertificates(new ByteArrayInputStream(certificateChain.getBytes()));
|
||||
List<X509Certificate> certificates = new LinkedList<>(certificatesCollection);
|
||||
PKIXParameters pkixParameters = new PKIXParameters(trustStore);
|
||||
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
|
||||
|
||||
if (certificates.isEmpty()) {
|
||||
throw new CertificateException("No certificates available! Badly-formatted cert chain?");
|
||||
}
|
||||
|
||||
this.path = certificateFactory.generateCertPath(certificates);
|
||||
|
||||
pkixParameters.setRevocationEnabled(false);
|
||||
|
|
|
@ -47,7 +47,7 @@ public class DiscoveryRequest {
|
|||
this.requestId = requestId;
|
||||
this.iv = iv;
|
||||
this.data = data;
|
||||
this. mac = mac;
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
public byte[] getRequestId() {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package org.whispersystems.signalservice.internal.contacts.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
|
||||
public class KeyBackupRequest {
|
||||
|
||||
@JsonProperty
|
||||
private byte[] requestId;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] iv;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] data;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] mac;
|
||||
|
||||
public KeyBackupRequest() {
|
||||
}
|
||||
|
||||
public KeyBackupRequest(byte[] requestId, byte[] iv, byte[] data, byte[] mac) {
|
||||
this.requestId = requestId;
|
||||
this.iv = iv;
|
||||
this.data = data;
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
public byte[] getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public byte[] getMac() {
|
||||
return mac;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "{ requestId: " + Hex.toString(requestId) + ", iv: " + Hex.toString(iv) + ", data: " + Hex.toString(data) + ", mac: " + Hex.toString(mac) + "}";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package org.whispersystems.signalservice.internal.contacts.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
|
||||
public class KeyBackupResponse {
|
||||
|
||||
@JsonProperty
|
||||
private byte[] iv;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] data;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] mac;
|
||||
|
||||
public KeyBackupResponse() {}
|
||||
|
||||
public KeyBackupResponse(byte[] iv, byte[] data, byte[] mac) {
|
||||
this.iv = iv;
|
||||
this.data = data;
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public byte[] getMac() {
|
||||
return mac;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "{iv: " + (iv == null ? null : Hex.toString(iv)) + ", data: " + (data == null ? null: Hex.toString(data)) + ", mac: " + (mac == null ? null : Hex.toString(mac)) + "}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package org.whispersystems.signalservice.internal.contacts.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class TokenResponse {
|
||||
|
||||
@JsonProperty
|
||||
private byte[] backupId;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] token;
|
||||
|
||||
@JsonProperty
|
||||
private int tries;
|
||||
|
||||
@JsonCreator
|
||||
public TokenResponse() {
|
||||
}
|
||||
|
||||
public TokenResponse(byte[] backupId, byte[] token, int tries) {
|
||||
this.backupId = backupId;
|
||||
this.token = token;
|
||||
this.tries = tries;
|
||||
}
|
||||
|
||||
public byte[] getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public byte[] getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public int getTries() {
|
||||
return tries;
|
||||
}
|
||||
}
|
|
@ -28,19 +28,23 @@ public class AccountAttributes {
|
|||
@JsonProperty
|
||||
private String pin;
|
||||
|
||||
@JsonProperty
|
||||
private String registrationLock;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] unidentifiedAccessKey;
|
||||
|
||||
@JsonProperty
|
||||
private boolean unrestrictedUnidentifiedAccess;
|
||||
|
||||
public AccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) {
|
||||
public AccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess) {
|
||||
this.signalingKey = signalingKey;
|
||||
this.registrationId = registrationId;
|
||||
this.voice = true;
|
||||
this.video = true;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.pin = pin;
|
||||
this.registrationLock = registrationLock;
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
|
||||
}
|
||||
|
@ -71,6 +75,10 @@ public class AccountAttributes {
|
|||
return pin;
|
||||
}
|
||||
|
||||
public String getRegistrationLock() {
|
||||
return registrationLock;
|
||||
}
|
||||
|
||||
public byte[] getUnidentifiedAccessKey() {
|
||||
return unidentifiedAccessKey;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import okhttp3.Credentials;
|
||||
|
||||
public class AuthCredentials {
|
||||
|
||||
@JsonProperty
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
private String password;
|
||||
|
||||
public String asBasic() {
|
||||
return Credentials.basic(username, password);
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class ContactDiscoveryCredentials {
|
||||
|
||||
@JsonProperty
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
private String password;
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
|
@ -3,14 +3,16 @@ package org.whispersystems.signalservice.internal.push;
|
|||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
public class LockedException extends NonSuccessfulResponseCodeException {
|
||||
public final class LockedException extends NonSuccessfulResponseCodeException {
|
||||
|
||||
private int length;
|
||||
private long timeRemaining;
|
||||
private final int length;
|
||||
private final long timeRemaining;
|
||||
private final String basicStorageCredentials;
|
||||
|
||||
LockedException(int length, long timeRemaining) {
|
||||
this.length = length;
|
||||
this.timeRemaining = timeRemaining;
|
||||
LockedException(int length, long timeRemaining, String basicStorageCredentials) {
|
||||
this.length = length;
|
||||
this.timeRemaining = timeRemaining;
|
||||
this.basicStorageCredentials = basicStorageCredentials;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
|
@ -20,4 +22,8 @@ public class LockedException extends NonSuccessfulResponseCodeException {
|
|||
public long getTimeRemaining() {
|
||||
return timeRemaining;
|
||||
}
|
||||
|
||||
public String getBasicStorageCredentials() {
|
||||
return basicStorageCredentials;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,8 +43,9 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
|
|||
import org.whispersystems.signalservice.internal.configuration.SignalUrl;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
|
||||
|
@ -78,14 +79,15 @@ import java.util.Set;
|
|||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.Credentials;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
@ -108,6 +110,7 @@ public class PushServiceSocket {
|
|||
private static final String TURN_SERVER_INFO = "/v1/accounts/turn";
|
||||
private static final String SET_ACCOUNT_ATTRIBUTES = "/v1/accounts/attributes/";
|
||||
private static final String PIN_PATH = "/v1/accounts/pin/";
|
||||
private static final String REGISTRATION_LOCK_PATH = "/v1/accounts/registration_lock";
|
||||
private static final String REQUEST_PUSH_CHALLENGE = "/v1/accounts/fcm/preauth/%s/%s";
|
||||
private static final String WHO_AM_I = "/v1/accounts/whoami";
|
||||
private static final String SET_USERNAME_PATH = "/v1/accounts/username/%s";
|
||||
|
@ -137,6 +140,8 @@ public class PushServiceSocket {
|
|||
private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery";
|
||||
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true";
|
||||
|
||||
private static final String KBS_AUTH_PATH = "/v1/backup/auth";
|
||||
|
||||
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
|
||||
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
|
||||
|
||||
|
@ -152,6 +157,7 @@ public class PushServiceSocket {
|
|||
private final ServiceConnectionHolder[] serviceClients;
|
||||
private final ConnectionHolder[] cdnClients;
|
||||
private final ConnectionHolder[] contactDiscoveryClients;
|
||||
private final ConnectionHolder[] keyBackupServiceClients;
|
||||
private final OkHttpClient attachmentClient;
|
||||
|
||||
private final CredentialsProvider credentialsProvider;
|
||||
|
@ -164,6 +170,7 @@ public class PushServiceSocket {
|
|||
this.serviceClients = createServiceConnectionHolders(signalServiceConfiguration.getSignalServiceUrls());
|
||||
this.cdnClients = createConnectionHolders(signalServiceConfiguration.getSignalCdnUrls());
|
||||
this.contactDiscoveryClients = createConnectionHolders(signalServiceConfiguration.getSignalContactDiscoveryUrls());
|
||||
this.keyBackupServiceClients = createConnectionHolders(signalServiceConfiguration.getSignalKeyBackupServiceUrls());
|
||||
this.attachmentClient = createAttachmentClient();
|
||||
this.random = new SecureRandom();
|
||||
}
|
||||
|
@ -219,11 +226,12 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, String pin,
|
||||
public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages,
|
||||
String pin, String registrationLock,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
|
||||
throws IOException
|
||||
{
|
||||
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
|
||||
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
|
||||
String requestBody = JsonUtil.toJson(signalingKeyEntity);
|
||||
String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody);
|
||||
VerifyAccountResponse response = JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
|
||||
|
@ -236,11 +244,16 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin,
|
||||
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages,
|
||||
String pin, String registrationLock,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
|
||||
throws IOException
|
||||
{
|
||||
AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin,
|
||||
if (registrationLock != null && pin != null) {
|
||||
throw new AssertionError("Pin should be null if registrationLock is set.");
|
||||
}
|
||||
|
||||
AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock,
|
||||
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
|
||||
makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes));
|
||||
}
|
||||
|
@ -282,10 +295,20 @@ public class PushServiceSocket {
|
|||
makeServiceRequest(PIN_PATH, "PUT", JsonUtil.toJson(accountLock));
|
||||
}
|
||||
|
||||
/** Note: Setting a KBS Pin will clear this */
|
||||
public void removePin() throws IOException {
|
||||
makeServiceRequest(PIN_PATH, "DELETE", null);
|
||||
}
|
||||
|
||||
public void setRegistrationLock(String registrationLock) throws IOException {
|
||||
RegistrationLockV2 accountLock = new RegistrationLockV2(registrationLock);
|
||||
makeServiceRequest(REGISTRATION_LOCK_PATH, "PUT", JsonUtil.toJson(accountLock));
|
||||
}
|
||||
|
||||
public void removePinV2() throws IOException {
|
||||
makeServiceRequest(REGISTRATION_LOCK_PATH, "DELETE", null);
|
||||
}
|
||||
|
||||
public byte[] getSenderCertificateLegacy() throws IOException {
|
||||
String responseText = makeServiceRequest(SENDER_CERTIFICATE_LEGACY_PATH, "GET", null);
|
||||
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
|
||||
|
@ -592,26 +615,27 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
public String getContactDiscoveryAuthorization() throws IOException {
|
||||
String response = makeServiceRequest(DIRECTORY_AUTH_PATH, "GET", null);
|
||||
ContactDiscoveryCredentials token = JsonUtil.fromJson(response, ContactDiscoveryCredentials.class);
|
||||
return Credentials.basic(token.getUsername(), token.getPassword());
|
||||
private String getCredentials(String authPath) throws IOException {
|
||||
String response = makeServiceRequest(authPath, "GET", null, NO_HEADERS);
|
||||
AuthCredentials token = JsonUtil.fromJson(response, AuthCredentials.class);
|
||||
return token.asBasic();
|
||||
}
|
||||
|
||||
public Pair<RemoteAttestationResponse, List<String>> getContactDiscoveryRemoteAttestation(String authorization, RemoteAttestationRequest request, String mrenclave)
|
||||
throws IOException
|
||||
{
|
||||
Response response = makeContactDiscoveryRequest(authorization, new LinkedList<String>(), "/v1/attestation/" + mrenclave, "PUT", JsonUtil.toJson(request));
|
||||
ResponseBody body = response.body();
|
||||
List<String> rawCookies = response.headers("Set-Cookie");
|
||||
List<String> cookies = new LinkedList<>();
|
||||
public String getContactDiscoveryAuthorization() throws IOException {
|
||||
return getCredentials(DIRECTORY_AUTH_PATH);
|
||||
}
|
||||
|
||||
for (String cookie : rawCookies) {
|
||||
cookies.add(cookie.split(";")[0]);
|
||||
}
|
||||
public String getKeyBackupServiceAuthorization() throws IOException {
|
||||
return getCredentials(KBS_AUTH_PATH);
|
||||
}
|
||||
|
||||
public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName)
|
||||
throws IOException
|
||||
{
|
||||
ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, null, "/v1/token/" + enclaveName, "GET", null).body();
|
||||
|
||||
if (body != null) {
|
||||
return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
|
||||
return JsonUtil.fromJson(body.string(), TokenResponse.class);
|
||||
} else {
|
||||
throw new NonSuccessfulResponseCodeException("Empty response!");
|
||||
}
|
||||
|
@ -620,7 +644,7 @@ public class PushServiceSocket {
|
|||
public DiscoveryResponse getContactDiscoveryRegisteredUsers(String authorizationToken, DiscoveryRequest request, List<String> cookies, String mrenclave)
|
||||
throws IOException
|
||||
{
|
||||
ResponseBody body = makeContactDiscoveryRequest(authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
|
||||
ResponseBody body = makeRequest(ClientSet.ContactDiscovery, authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
|
||||
|
||||
if (body != null) {
|
||||
return JsonUtil.fromJson(body.string(), DiscoveryResponse.class);
|
||||
|
@ -629,6 +653,18 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
public KeyBackupResponse putKbsData(String authorizationToken, KeyBackupRequest request, List<String> cookies, String mrenclave)
|
||||
throws IOException
|
||||
{
|
||||
ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, cookies, "/v1/backup/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
|
||||
|
||||
if (body != null) {
|
||||
return JsonUtil.fromJson(body.string(), KeyBackupResponse.class);
|
||||
} else {
|
||||
throw new NonSuccessfulResponseCodeException("Empty response!");
|
||||
}
|
||||
}
|
||||
|
||||
public void reportContactDiscoveryServiceMatch() throws IOException {
|
||||
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "ok"), "PUT", "");
|
||||
}
|
||||
|
@ -922,7 +958,12 @@ public class PushServiceSocket {
|
|||
throw new PushNetworkException(e);
|
||||
}
|
||||
|
||||
throw new LockedException(accountLockFailure.length, accountLockFailure.timeRemaining);
|
||||
AuthCredentials credentials = accountLockFailure.backupCredentials;
|
||||
String basicStorageCredentials = credentials != null ? credentials.asBasic() : null;
|
||||
|
||||
throw new LockedException(accountLockFailure.length,
|
||||
accountLockFailure.timeRemaining,
|
||||
basicStorageCredentials);
|
||||
}
|
||||
|
||||
if (responseCode != 200 && responseCode != 204) {
|
||||
|
@ -960,10 +1001,12 @@ public class PushServiceSocket {
|
|||
request.addHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
|
||||
} else if (credentialsProvider.getPassword() != null) {
|
||||
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
|
||||
if (!headers.containsKey("Authorization")) {
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
|
||||
} else if (credentialsProvider.getPassword() != null) {
|
||||
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
|
||||
}
|
||||
}
|
||||
|
||||
if (userAgent != null) {
|
||||
|
@ -992,15 +1035,33 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
private Response makeContactDiscoveryRequest(String authorization, List<String> cookies, String path, String method, String body)
|
||||
private ConnectionHolder[] clientsFor(ClientSet clientSet) {
|
||||
switch (clientSet) {
|
||||
case ContactDiscovery:
|
||||
return contactDiscoveryClients;
|
||||
case KeyBackup:
|
||||
return keyBackupServiceClients;
|
||||
default:
|
||||
throw new AssertionError("Unknown attestation purpose");
|
||||
}
|
||||
}
|
||||
|
||||
Response makeRequest(ClientSet clientSet, String authorization, List<String> cookies, String path, String method, String body)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
ConnectionHolder connectionHolder = getRandom(contactDiscoveryClients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
ConnectionHolder connectionHolder = getRandom(clientsFor(clientSet), random);
|
||||
|
||||
return makeRequest(connectionHolder, authorization, cookies, path, method, body);
|
||||
}
|
||||
|
||||
private Response makeRequest(ConnectionHolder connectionHolder, String authorization, List<String> cookies, String path, String method, String body)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
|
||||
|
||||
|
@ -1153,12 +1214,26 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
private static class RegistrationLockV2 {
|
||||
@JsonProperty
|
||||
private String registrationLock;
|
||||
|
||||
public RegistrationLockV2() {}
|
||||
|
||||
public RegistrationLockV2(String registrationLock) {
|
||||
this.registrationLock = registrationLock;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RegistrationLockFailure {
|
||||
@JsonProperty
|
||||
private int length;
|
||||
|
||||
@JsonProperty
|
||||
private long timeRemaining;
|
||||
|
||||
@JsonProperty
|
||||
private AuthCredentials backupCredentials;
|
||||
}
|
||||
|
||||
private static class AttachmentDescriptor {
|
||||
|
@ -1225,4 +1300,6 @@ public class PushServiceSocket {
|
|||
@Override
|
||||
public void handle(int responseCode) { }
|
||||
}
|
||||
|
||||
public enum ClientSet { ContactDiscovery, KeyBackup }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import org.whispersystems.curve25519.Curve25519;
|
||||
import org.whispersystems.curve25519.Curve25519KeyPair;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationCipher;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SignatureException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public final class RemoteAttestationUtil {
|
||||
|
||||
private RemoteAttestationUtil() {
|
||||
}
|
||||
|
||||
public static RemoteAttestation getAndVerifyRemoteAttestation(PushServiceSocket socket,
|
||||
PushServiceSocket.ClientSet clientSet,
|
||||
KeyStore iasKeyStore,
|
||||
String enclaveName,
|
||||
String mrenclave,
|
||||
String authorization)
|
||||
throws IOException, Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException
|
||||
{
|
||||
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
|
||||
Curve25519KeyPair keyPair = curve.generateKeyPair();
|
||||
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
|
||||
Pair<RemoteAttestationResponse, List<String>> attestationResponsePair = getRemoteAttestation(socket, clientSet, authorization, attestationRequest, enclaveName);
|
||||
RemoteAttestationResponse attestationResponse = attestationResponsePair.first();
|
||||
List<String> attestationCookies = attestationResponsePair.second();
|
||||
|
||||
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.getServerEphemeralPublic(), attestationResponse.getServerStaticPublic());
|
||||
Quote quote = new Quote(attestationResponse.getQuote());
|
||||
byte[] requestId = RemoteAttestationCipher.getRequestId(keys, attestationResponse);
|
||||
|
||||
RemoteAttestationCipher.verifyServerQuote(quote, attestationResponse.getServerStaticPublic(), mrenclave);
|
||||
|
||||
RemoteAttestationCipher.verifyIasSignature(iasKeyStore, attestationResponse.getCertificates(), attestationResponse.getSignatureBody(), attestationResponse.getSignature(), quote);
|
||||
|
||||
return new RemoteAttestation(requestId, keys, attestationCookies);
|
||||
}
|
||||
|
||||
private static Pair<RemoteAttestationResponse, List<String>> getRemoteAttestation(PushServiceSocket socket,
|
||||
PushServiceSocket.ClientSet clientSet,
|
||||
String authorization,
|
||||
RemoteAttestationRequest request,
|
||||
String enclaveName)
|
||||
throws IOException
|
||||
{
|
||||
Response response = socket.makeRequest(clientSet, authorization, new LinkedList<String>(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(request));
|
||||
ResponseBody body = response.body();
|
||||
List<String> rawCookies = response.headers("Set-Cookie");
|
||||
List<String> cookies = new LinkedList<>();
|
||||
|
||||
for (String cookie : rawCookies) {
|
||||
cookies.add(cookie.split(";")[0]);
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
|
||||
} else {
|
||||
throw new NonSuccessfulResponseCodeException("Empty response!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.whispersystems.signalservice.internal.registrationpin;
|
||||
|
||||
public final class InvalidPinException extends Exception {
|
||||
|
||||
InvalidPinException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package org.whispersystems.signalservice.internal.registrationpin;
|
||||
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public final class PinStretcher {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
public static StretchedPin stretchPin(CharSequence pin) throws InvalidPinException {
|
||||
return new StretchedPin(pin);
|
||||
}
|
||||
|
||||
public static class StretchedPin {
|
||||
private final byte[] stretchedPin;
|
||||
private final byte[] pinKey1;
|
||||
private final byte[] kbsAccessKey;
|
||||
|
||||
private StretchedPin(byte[] stretchedPin, byte[] pinKey1, byte[] kbsAccessKey) {
|
||||
this.stretchedPin = stretchedPin;
|
||||
this.pinKey1 = pinKey1;
|
||||
this.kbsAccessKey = kbsAccessKey;
|
||||
}
|
||||
|
||||
private StretchedPin(CharSequence pin) throws InvalidPinException {
|
||||
if (pin.length() < 4) throw new InvalidPinException("Pin too short");
|
||||
|
||||
char[] arabicPin = toArabic(pin);
|
||||
|
||||
stretchedPin = pbkdf2HmacSHA256(arabicPin, "nosalt", 20000, 256);
|
||||
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA256);
|
||||
mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256));
|
||||
mac.update("Master Key Encryption".getBytes(UTF_8));
|
||||
|
||||
pinKey1 = new byte[32];
|
||||
mac.doFinal(pinKey1, 0);
|
||||
|
||||
mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256));
|
||||
mac.update("KBS Access Key".getBytes(UTF_8));
|
||||
|
||||
kbsAccessKey = new byte[32];
|
||||
mac.doFinal(kbsAccessKey, 0);
|
||||
} catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public MasterKey withPinKey2(byte[] pinKey2) {
|
||||
return new MasterKey(pinKey1, pinKey2, this);
|
||||
}
|
||||
|
||||
public MasterKey withNewSecurePinKey2() {
|
||||
return withPinKey2(Util.getSecretBytes(32));
|
||||
}
|
||||
|
||||
public byte[] getPinKey1() {
|
||||
return pinKey1;
|
||||
}
|
||||
|
||||
public byte[] getStretchedPin() {
|
||||
return stretchedPin;
|
||||
}
|
||||
|
||||
public byte[] getKbsAccessKey() {
|
||||
return kbsAccessKey;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MasterKey extends StretchedPin {
|
||||
private final byte[] pinKey2;
|
||||
private final byte[] masterKey;
|
||||
private final String registrationLock;
|
||||
|
||||
private MasterKey(byte[] pinKey1, byte[] pinKey2, StretchedPin stretchedPin) {
|
||||
super(stretchedPin.stretchedPin, stretchedPin.pinKey1, stretchedPin.kbsAccessKey);
|
||||
|
||||
if (pinKey2.length != 32) {
|
||||
throw new AssertionError("PinKey2 must be exactly 32 bytes");
|
||||
}
|
||||
|
||||
this.pinKey2 = pinKey2.clone();
|
||||
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA256);
|
||||
|
||||
mac.init(new SecretKeySpec(pinKey1, HMAC_SHA256));
|
||||
mac.update(pinKey2);
|
||||
|
||||
masterKey = new byte[32];
|
||||
mac.doFinal(masterKey, 0);
|
||||
|
||||
mac.init(new SecretKeySpec(masterKey, HMAC_SHA256));
|
||||
mac.update("Registration Lock".getBytes(UTF_8));
|
||||
|
||||
byte[] registration_lock_token_bytes = new byte[32];
|
||||
mac.doFinal(registration_lock_token_bytes, 0);
|
||||
registrationLock = Hex.toStringCondensed(registration_lock_token_bytes);
|
||||
|
||||
} catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getPinKey2() {
|
||||
return pinKey2;
|
||||
}
|
||||
|
||||
public String getRegistrationLock() {
|
||||
return registrationLock;
|
||||
}
|
||||
|
||||
public byte[] getMasterKey() {
|
||||
return masterKey;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] pbkdf2HmacSHA256(char[] pin, String salt, int iterationCount, int outputSize) {
|
||||
byte[] saltBytes = salt.getBytes(Charset.forName("UTF-8"));
|
||||
|
||||
try {
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
PBEKeySpec spec = new PBEKeySpec(pin, saltBytes, iterationCount, outputSize);
|
||||
byte[] encoded = skf.generateSecret(spec).getEncoded();
|
||||
|
||||
spec.clearPassword();
|
||||
|
||||
return encoded;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new AssertionError("Could not stretch pin", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string of not necessarily Arabic numerals to Arabic 0..9 characters.
|
||||
*/
|
||||
private static char[] toArabic(CharSequence numerals) throws InvalidPinException {
|
||||
int length = numerals.length();
|
||||
char[] arabic = new char[length];
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int digit = Character.digit(numerals.charAt(i), 10);
|
||||
|
||||
if (digit < 0) {
|
||||
throw new InvalidPinException("Pin must only consist of decimals");
|
||||
}
|
||||
|
||||
arabic[i] = (char) ('0' + digit);
|
||||
}
|
||||
|
||||
return arabic;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
@ -71,13 +70,9 @@ public class Util {
|
|||
}
|
||||
|
||||
public static byte[] getSecretBytes(int size) {
|
||||
try {
|
||||
byte[] secret = new byte[size];
|
||||
SecureRandom.getInstance("SHA1PRNG").nextBytes(secret);
|
||||
return secret;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
byte[] secret = new byte[size];
|
||||
new SecureRandom().nextBytes(secret);
|
||||
return secret;
|
||||
}
|
||||
|
||||
public static byte[] getRandomLengthBytes(int maxSize) {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,31 @@
|
|||
package org.whispersystems.signalservice.internal.registrationpin;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class PinStretchFailureTest {
|
||||
|
||||
@Test(expected = InvalidPinException.class)
|
||||
public void non_numeric_pin() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("A");
|
||||
}
|
||||
|
||||
@Test(expected = InvalidPinException.class)
|
||||
public void empty() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("");
|
||||
}
|
||||
|
||||
@Test(expected = InvalidPinException.class)
|
||||
public void too_few_digits() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("123");
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void pin_key_2_too_short() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("0000").withPinKey2(new byte[31]);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void pin_key_2_too_long() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("0000").withPinKey2(new byte[33]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package org.whispersystems.signalservice.internal.registrationpin;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public final class PinStretchTest {
|
||||
|
||||
private final String pin;
|
||||
private final byte[] expectedStretchedPin;
|
||||
private final byte[] expectedKeyPin1;
|
||||
private final byte[] pinKey2;
|
||||
private final byte[] expectedMasterKey;
|
||||
private final String expectedRegistrationLock;
|
||||
private final byte[] expectedKbsAccessKey;
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[]{
|
||||
"12345",
|
||||
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
|
||||
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
|
||||
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
|
||||
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
|
||||
},
|
||||
new Object[]{
|
||||
"12345",
|
||||
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
|
||||
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"01198dc427cbf9c6b47f344654d75a263e53b992db73be44b201f357d072dc38",
|
||||
"bd1f4e129cc705c26c2fcebd3fbc6e7db60caade89e6c465c68ed60aeedbb0c3",
|
||||
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
|
||||
},
|
||||
new Object[]{
|
||||
"١٢٣٤٥",
|
||||
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
|
||||
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
|
||||
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
|
||||
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
|
||||
},
|
||||
new Object[]{
|
||||
"9876543210",
|
||||
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
|
||||
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"127a435c15be2528f4b735423f8ee558b789e8ea1f6fe64d144d5b21a87c4e06",
|
||||
"348d327acb823b54a988cf6bea647a154e21da25cbb121a115c13b871dccd548",
|
||||
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
|
||||
},
|
||||
new Object[]{
|
||||
"9876543210",
|
||||
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
|
||||
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"128833dbde1af3da703852b6b5a845e226fe9c7e069427b9c1e41279c0cdfb3a",
|
||||
"6be0b17899cfb5c4316b92acc7db3b6a2fa5b9a19ef3e58a1c84a4de49230aa6",
|
||||
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
|
||||
},
|
||||
new Object[]{
|
||||
"0123",
|
||||
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
|
||||
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
|
||||
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
|
||||
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
|
||||
},
|
||||
new Object[]{
|
||||
"௦௧௨௩",
|
||||
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
|
||||
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
|
||||
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
|
||||
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
|
||||
});
|
||||
}
|
||||
|
||||
public PinStretchTest(String pin,
|
||||
String expectedStretchedPin,
|
||||
String expectedKeyPin1,
|
||||
String pinKey2,
|
||||
String expectedMasterKey,
|
||||
String expectedRegistrationLock,
|
||||
String expectedKbsAccessKey) throws IOException {
|
||||
this.pin = pin;
|
||||
this.expectedStretchedPin = Hex.fromStringCondensed(expectedStretchedPin);
|
||||
this.expectedKeyPin1 = Hex.fromStringCondensed(expectedKeyPin1);
|
||||
this.pinKey2 = Hex.fromStringCondensed(pinKey2);
|
||||
this.expectedMasterKey = Hex.fromStringCondensed(expectedMasterKey);
|
||||
this.expectedRegistrationLock = expectedRegistrationLock;
|
||||
this.expectedKbsAccessKey = Hex.fromStringCondensed(expectedKbsAccessKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretch_pin() throws InvalidPinException {
|
||||
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
|
||||
|
||||
assertArrayEquals(expectedStretchedPin, stretchedPin.getStretchedPin());
|
||||
assertArrayEquals(expectedKeyPin1, stretchedPin.getPinKey1());
|
||||
assertArrayEquals(expectedKbsAccessKey, stretchedPin.getKbsAccessKey());
|
||||
|
||||
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(pinKey2);
|
||||
|
||||
assertArrayEquals(pinKey2, masterKey.getPinKey2());
|
||||
assertArrayEquals(expectedMasterKey, masterKey.getMasterKey());
|
||||
assertEquals(expectedRegistrationLock, masterKey.getRegistrationLock());
|
||||
|
||||
assertArrayEquals(expectedStretchedPin, masterKey.getStretchedPin());
|
||||
assertArrayEquals(expectedKeyPin1, masterKey.getPinKey1());
|
||||
assertArrayEquals(expectedKbsAccessKey, masterKey.getKbsAccessKey());
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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>() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue