mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 17:38:04 +01:00
Add storage capability and return KBS creds on rereg w/ storage set
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class StoredRegistrationLock {
|
||||
|
||||
private final Optional<String> registrationLock;
|
||||
|
||||
private final Optional<String> registrationLockSalt;
|
||||
|
||||
private final Optional<String> deprecatedPin;
|
||||
|
||||
private final long lastSeen;
|
||||
|
||||
public StoredRegistrationLock(Optional<String> registrationLock, Optional<String> registrationLockSalt, Optional<String> deprecatedPin, long lastSeen) {
|
||||
this.registrationLock = registrationLock;
|
||||
this.registrationLockSalt = registrationLockSalt;
|
||||
this.deprecatedPin = deprecatedPin;
|
||||
this.lastSeen = lastSeen;
|
||||
}
|
||||
|
||||
public boolean requiresClientRegistrationLock() {
|
||||
return ((registrationLock.isPresent() && registrationLockSalt.isPresent()) || deprecatedPin.isPresent()) && System.currentTimeMillis() - lastSeen < TimeUnit.DAYS.toMillis(7);
|
||||
}
|
||||
|
||||
public boolean needsFailureCredentials() {
|
||||
return registrationLock.isPresent() && registrationLockSalt.isPresent();
|
||||
}
|
||||
|
||||
public long getTimeRemaining() {
|
||||
return TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - lastSeen);
|
||||
}
|
||||
|
||||
public boolean verify(@Nullable String clientRegistrationLock, @Nullable String clientDeprecatedPin) {
|
||||
if (Util.isEmpty(clientRegistrationLock) && Util.isEmpty(clientDeprecatedPin)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (registrationLock.isPresent() && registrationLockSalt.isPresent() && !Util.isEmpty(clientRegistrationLock)) {
|
||||
return new AuthenticationCredentials(registrationLock.get(), registrationLockSalt.get()).verify(clientRegistrationLock);
|
||||
} else if (deprecatedPin.isPresent() && !Util.isEmpty(clientDeprecatedPin)) {
|
||||
return MessageDigest.isEqual(deprecatedPin.get().getBytes(), clientDeprecatedPin.getBytes());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public StoredRegistrationLock forTime(long timestamp) {
|
||||
return new StoredRegistrationLock(registrationLock, registrationLockSalt, deprecatedPin, timestamp);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
@@ -74,14 +75,12 @@ import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.auth.Auth;
|
||||
@@ -270,48 +269,26 @@ public class AccountController {
|
||||
|
||||
Optional<StoredVerificationCode> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
|
||||
|
||||
if (!storedVerificationCode.isPresent() || !storedVerificationCode.get().isValid(verificationCode)) {
|
||||
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(verificationCode)) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
Optional<Account> existingAccount = accounts.get(number);
|
||||
Optional<Account> existingAccount = accounts.get(number);
|
||||
Optional<StoredRegistrationLock> existingRegistrationLock = existingAccount.map(Account::getRegistrationLock);
|
||||
Optional<ExternalServiceCredentials> existingBackupCredentials = existingAccount.map(Account::getUuid)
|
||||
.map(uuid -> backupServiceCredentialGenerator.generateFor(uuid.toString()));
|
||||
|
||||
if (existingAccount.isPresent() &&
|
||||
(existingAccount.get().getPin().isPresent() || existingAccount.get().getRegistrationLock().isPresent()) &&
|
||||
System.currentTimeMillis() - existingAccount.get().getLastSeen() < TimeUnit.DAYS.toMillis(7))
|
||||
{
|
||||
if (existingRegistrationLock.isPresent() && existingRegistrationLock.get().requiresClientRegistrationLock()) {
|
||||
rateLimiters.getVerifyLimiter().clear(number);
|
||||
|
||||
long timeRemaining = TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - existingAccount.get().getLastSeen());
|
||||
Optional<ExternalServiceCredentials> credentials = existingAccount.get().getRegistrationLock().isPresent() &&
|
||||
existingAccount.get().getRegistrationLockSalt().isPresent() ?
|
||||
Optional.of(backupServiceCredentialGenerator.generateFor(existingAccount.get().getUuid().toString())) :
|
||||
Optional.empty();
|
||||
|
||||
if (Util.isEmpty(accountAttributes.getPin()) &&
|
||||
Util.isEmpty(accountAttributes.getRegistrationLock()))
|
||||
{
|
||||
throw new WebApplicationException(Response.status(423)
|
||||
.entity(new RegistrationLockFailure(timeRemaining, credentials.orElse(null)))
|
||||
.build());
|
||||
if (!Util.isEmpty(accountAttributes.getRegistrationLock()) || !Util.isEmpty(accountAttributes.getPin())) {
|
||||
rateLimiters.getPinLimiter().validate(number);
|
||||
}
|
||||
|
||||
rateLimiters.getPinLimiter().validate(number);
|
||||
|
||||
boolean pinMatches;
|
||||
|
||||
if (existingAccount.get().getRegistrationLock().isPresent() && existingAccount.get().getRegistrationLockSalt().isPresent()) {
|
||||
pinMatches = new AuthenticationCredentials(existingAccount.get().getRegistrationLock().get(),
|
||||
existingAccount.get().getRegistrationLockSalt().get()).verify(accountAttributes.getRegistrationLock());
|
||||
} else if (existingAccount.get().getPin().isPresent()) {
|
||||
pinMatches = MessageDigest.isEqual(existingAccount.get().getPin().get().getBytes(), accountAttributes.getPin().getBytes());
|
||||
} else {
|
||||
throw new AssertionError("Invalid registration lock state");
|
||||
}
|
||||
|
||||
if (!pinMatches) {
|
||||
if (!existingRegistrationLock.get().verify(accountAttributes.getRegistrationLock(), accountAttributes.getPin())) {
|
||||
throw new WebApplicationException(Response.status(423)
|
||||
.entity(new RegistrationLockFailure(timeRemaining, credentials.orElse(null)))
|
||||
.entity(new RegistrationLockFailure(existingRegistrationLock.get().getTimeRemaining(),
|
||||
existingRegistrationLock.get().needsFailureCredentials() ? existingBackupCredentials.orElseThrow() : null))
|
||||
.build());
|
||||
}
|
||||
|
||||
@@ -322,7 +299,7 @@ public class AccountController {
|
||||
|
||||
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark();
|
||||
|
||||
return new AccountCreationResult(account.getUuid());
|
||||
return new AccountCreationResult(account.getUuid(), existingAccount.map(Account::isStorageSupported).orElse(false) ? existingBackupCredentials.orElse(null) : null);
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
logger.info("Bad Authorization Header", e);
|
||||
throw new WebApplicationException(Response.status(401).build());
|
||||
@@ -423,8 +400,7 @@ public class AccountController {
|
||||
@Path("/registration_lock")
|
||||
public void setRegistrationLock(@Auth Account account, @Valid RegistrationLock accountLock) {
|
||||
AuthenticationCredentials credentials = new AuthenticationCredentials(accountLock.getRegistrationLock());
|
||||
account.setRegistrationLock(credentials.getHashedAuthenticationToken());
|
||||
account.setRegistrationLockSalt(credentials.getSalt());
|
||||
account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
|
||||
account.setPin(null);
|
||||
|
||||
accounts.update(account);
|
||||
@@ -434,8 +410,7 @@ public class AccountController {
|
||||
@DELETE
|
||||
@Path("/registration_lock")
|
||||
public void removeRegistrationLock(@Auth Account account) {
|
||||
account.setRegistrationLock(null);
|
||||
account.setRegistrationLockSalt(null);
|
||||
account.setRegistrationLock(null, null);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@@ -445,8 +420,7 @@ public class AccountController {
|
||||
@Path("/pin/")
|
||||
public void setPin(@Auth Account account, @Valid DeprecatedPin accountLock) {
|
||||
account.setPin(accountLock.getPin());
|
||||
account.setRegistrationLock(null);
|
||||
account.setRegistrationLockSalt(null);
|
||||
account.setRegistrationLock(null, null);
|
||||
|
||||
accounts.update(account);
|
||||
}
|
||||
@@ -500,12 +474,10 @@ public class AccountController {
|
||||
account.setPin(attributes.getPin());
|
||||
} else if (!Util.isEmpty(attributes.getRegistrationLock())) {
|
||||
AuthenticationCredentials credentials = new AuthenticationCredentials(attributes.getRegistrationLock());
|
||||
account.setRegistrationLock(credentials.getHashedAuthenticationToken());
|
||||
account.setRegistrationLockSalt(credentials.getSalt());
|
||||
account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
|
||||
} else {
|
||||
account.setPin(null);
|
||||
account.setRegistrationLock(null);
|
||||
account.setRegistrationLockSalt(null);
|
||||
account.setRegistrationLock(null, null);
|
||||
}
|
||||
|
||||
account.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
|
||||
@@ -518,7 +490,7 @@ public class AccountController {
|
||||
@Path("/whoami")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AccountCreationResult whoAmI(@Auth Account account) {
|
||||
return new AccountCreationResult(account.getUuid());
|
||||
return new AccountCreationResult(account.getUuid(), backupServiceCredentialGenerator.generateFor(account.getUuid().toString()));
|
||||
}
|
||||
|
||||
@DELETE
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class AccountCreationResult {
|
||||
@@ -9,13 +11,21 @@ public class AccountCreationResult {
|
||||
@JsonProperty
|
||||
private UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
private ExternalServiceCredentials backupCredentials;
|
||||
|
||||
public AccountCreationResult() {}
|
||||
|
||||
public AccountCreationResult(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
public AccountCreationResult(UUID uuid, ExternalServiceCredentials backupCredentials) {
|
||||
this.uuid = uuid;
|
||||
this.backupCredentials = backupCredentials;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public ExternalServiceCredentials getBackupCredentials() {
|
||||
return backupCredentials;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import java.security.Principal;
|
||||
@@ -144,6 +145,10 @@ public class Account implements Principal {
|
||||
.allMatch(device -> device.getCapabilities() != null && device.getCapabilities().isGv2());
|
||||
}
|
||||
|
||||
public boolean isStorageSupported() {
|
||||
return devices.stream().anyMatch(device -> device.getCapabilities() != null && device.getCapabilities().isStorage());
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return
|
||||
getMasterDevice().isPresent() &&
|
||||
@@ -227,30 +232,19 @@ public class Account implements Principal {
|
||||
this.avatarDigest = avatarDigest;
|
||||
}
|
||||
|
||||
public Optional<String> getPin() {
|
||||
return Optional.ofNullable(pin);
|
||||
}
|
||||
|
||||
public void setPin(String pin) {
|
||||
this.pin = pin;
|
||||
}
|
||||
|
||||
public void setRegistrationLock(String registrationLock) {
|
||||
this.registrationLock = registrationLock;
|
||||
}
|
||||
|
||||
public Optional<String> getRegistrationLock() {
|
||||
return Optional.ofNullable(registrationLock);
|
||||
}
|
||||
|
||||
public void setRegistrationLockSalt(String registrationLockSalt) {
|
||||
public void setRegistrationLock(String registrationLock, String registrationLockSalt) {
|
||||
this.registrationLock = registrationLock;
|
||||
this.registrationLockSalt = registrationLockSalt;
|
||||
}
|
||||
|
||||
public Optional<String> getRegistrationLockSalt() {
|
||||
return Optional.ofNullable(registrationLockSalt);
|
||||
public StoredRegistrationLock getRegistrationLock() {
|
||||
return new StoredRegistrationLock(Optional.ofNullable(registrationLock), Optional.ofNullable(registrationLockSalt), Optional.ofNullable(pin), getLastSeen());
|
||||
}
|
||||
|
||||
|
||||
public Optional<byte[]> getUnidentifiedAccessKey() {
|
||||
return Optional.ofNullable(unidentifiedAccessKey);
|
||||
}
|
||||
|
||||
@@ -273,11 +273,15 @@ public class Device {
|
||||
@JsonProperty
|
||||
private boolean gv2;
|
||||
|
||||
@JsonProperty
|
||||
private boolean storage;
|
||||
|
||||
public DeviceCapabilities() {}
|
||||
|
||||
public DeviceCapabilities(boolean uuid, boolean gv2) {
|
||||
this.uuid = uuid;
|
||||
this.gv2 = gv2;
|
||||
public DeviceCapabilities(boolean uuid, boolean gv2, boolean storage) {
|
||||
this.uuid = uuid;
|
||||
this.gv2 = gv2;
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
public boolean isUuid() {
|
||||
@@ -287,6 +291,10 @@ public class Device {
|
||||
public boolean isGv2() {
|
||||
return gv2;
|
||||
}
|
||||
|
||||
public boolean isStorage() {
|
||||
return storage;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user