Support for v2 registration lock
This commit is contained in:
parent
4fdbe9b9ff
commit
11902dec3c
|
@ -156,6 +156,11 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@JsonProperty
|
||||
private RecaptchaConfiguration recaptcha;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private SecureStorageServiceConfiguration storageService;
|
||||
|
||||
private Map<String, String> transparentDataIndex = new HashMap<>();
|
||||
|
||||
public RecaptchaConfiguration getRecaptchaConfiguration() {
|
||||
|
@ -194,6 +199,10 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return directory;
|
||||
}
|
||||
|
||||
public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() {
|
||||
return storageService;
|
||||
}
|
||||
|
||||
public AccountDatabaseCrawlerConfiguration getAccountDatabaseCrawlerConfiguration() {
|
||||
return accountDatabaseCrawler;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.jdbi.v3.core.Jdbi;
|
|||
import org.whispersystems.dispatch.DispatchManager;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
|
@ -44,6 +44,7 @@ import org.whispersystems.textsecuregcm.controllers.KeysController;
|
|||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ProfileController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.TransparentDataController;
|
||||
import org.whispersystems.textsecuregcm.controllers.VoiceVerificationController;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
|
@ -204,6 +205,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||
|
||||
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret(),
|
||||
true);
|
||||
|
||||
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
|
||||
|
||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerClient, apnSender, accountsManager);
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
||||
|
@ -212,8 +219,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
|
||||
RecaptchaClient recaptchaClient = new RecaptchaClient(config.getRecaptchaConfiguration().getSecret());
|
||||
|
||||
DirectoryCredentialsGenerator directoryCredentialsGenerator = new DirectoryCredentialsGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret());
|
||||
|
||||
DirectoryReconciliationClient directoryReconciliationClient = new DirectoryReconciliationClient(config.getDirectoryConfiguration().getDirectoryServerConfiguration());
|
||||
|
||||
ActiveUserCounter activeUserCounter = new ActiveUserCounter(config.getMetricsFactory(), cacheClient);
|
||||
|
@ -247,13 +253,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
DisabledPermittedAccount.class, disabledPermittedAccountAuthFilter)));
|
||||
environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)));
|
||||
|
||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender));
|
||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, storageCredentialsGenerator));
|
||||
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
|
||||
environment.jersey().register(new DirectoryController(rateLimiters, directory, directoryCredentialsGenerator));
|
||||
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
|
||||
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays())));
|
||||
environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales()));
|
||||
environment.jersey().register(new TransparentDataController(accountsManager, config.getTransparentDataIndex()));
|
||||
environment.jersey().register(new SecureStorageController(storageCredentialsGenerator));
|
||||
environment.jersey().register(attachmentControllerV1);
|
||||
environment.jersey().register(attachmentControllerV2);
|
||||
environment.jersey().register(keysController);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -17,18 +17,14 @@
|
|||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class AuthenticationCredentials {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AuthenticationCredentials.class);
|
||||
|
||||
private final String hashedAuthenticationToken;
|
||||
private final String salt;
|
||||
|
||||
|
@ -38,7 +34,7 @@ public class AuthenticationCredentials {
|
|||
}
|
||||
|
||||
public AuthenticationCredentials(String authenticationToken) {
|
||||
this.salt = Math.abs(new SecureRandom().nextInt()) + "";
|
||||
this.salt = String.valueOf(Math.abs(new SecureRandom().nextInt()));
|
||||
this.hashedAuthenticationToken = getHashedValue(salt, authenticationToken);
|
||||
}
|
||||
|
||||
|
@ -52,19 +48,13 @@ public class AuthenticationCredentials {
|
|||
|
||||
public boolean verify(String authenticationToken) {
|
||||
String theirValue = getHashedValue(salt, authenticationToken);
|
||||
|
||||
logger.debug("Comparing: " + theirValue + " , " + this.hashedAuthenticationToken);
|
||||
|
||||
return theirValue.equals(this.hashedAuthenticationToken);
|
||||
return MessageDigest.isEqual(theirValue.getBytes(StandardCharsets.UTF_8), this.hashedAuthenticationToken.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static String getHashedValue(String salt, String token) {
|
||||
Logger logger = LoggerFactory.getLogger(AuthenticationCredentials.class);
|
||||
logger.debug("Getting hashed token: " + salt + " , " + token);
|
||||
|
||||
try {
|
||||
return new String(Hex.encodeHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes("UTF-8"))));
|
||||
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
|
||||
return new String(Hex.encodeHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes(StandardCharsets.UTF_8))));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -14,27 +13,29 @@ import java.security.MessageDigest;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DirectoryCredentialsGenerator {
|
||||
public class ExternalServiceCredentialGenerator {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DirectoryCredentialsGenerator.class);
|
||||
private final Logger logger = LoggerFactory.getLogger(ExternalServiceCredentialGenerator.class);
|
||||
|
||||
private final byte[] key;
|
||||
private final byte[] userIdKey;
|
||||
private final boolean usernameDerivation;
|
||||
|
||||
public DirectoryCredentialsGenerator(byte[] key, byte[] userIdKey) {
|
||||
this.key = key;
|
||||
this.userIdKey = userIdKey;
|
||||
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) {
|
||||
this.key = key;
|
||||
this.userIdKey = userIdKey;
|
||||
this.usernameDerivation = usernameDerivation;
|
||||
}
|
||||
|
||||
public DirectoryCredentials generateFor(String number) {
|
||||
public ExternalServiceCredentials generateFor(String number) {
|
||||
Mac mac = getMacInstance();
|
||||
String username = getUserId(number, mac);
|
||||
String username = getUserId(number, mac, usernameDerivation);
|
||||
long currentTimeSeconds = System.currentTimeMillis() / 1000;
|
||||
String prefix = username + ":" + currentTimeSeconds;
|
||||
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
|
||||
String token = prefix + ":" + output;
|
||||
|
||||
return new DirectoryCredentials(username, token);
|
||||
return new ExternalServiceCredentials(username, token);
|
||||
}
|
||||
|
||||
|
||||
|
@ -46,7 +47,7 @@ public class DirectoryCredentialsGenerator {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!getUserId(number, mac).equals(parts[0])) {
|
||||
if (!getUserId(number, mac, usernameDerivation).equals(parts[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -57,8 +58,9 @@ public class DirectoryCredentialsGenerator {
|
|||
return isValidSignature(parts[0] + ":" + parts[1], parts[2], mac);
|
||||
}
|
||||
|
||||
private String getUserId(String number, Mac mac) {
|
||||
return Hex.encodeHexString(Util.truncate(getHmac(userIdKey, number.getBytes(), mac), 10));
|
||||
private String getUserId(String number, Mac mac, boolean usernameDerivation) {
|
||||
if (usernameDerivation) return Hex.encodeHexString(Util.truncate(getHmac(userIdKey, number.getBytes(), mac), 10));
|
||||
else return number;
|
||||
}
|
||||
|
||||
private boolean isValidTime(String timeString, long currentTimeMillis) {
|
|
@ -3,7 +3,7 @@ package org.whispersystems.textsecuregcm.auth;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class DirectoryCredentials {
|
||||
public class ExternalServiceCredentials {
|
||||
|
||||
@JsonProperty
|
||||
private String username;
|
||||
|
@ -11,12 +11,12 @@ public class DirectoryCredentials {
|
|||
@JsonProperty
|
||||
private String password;
|
||||
|
||||
public DirectoryCredentials(String username, String password) {
|
||||
public ExternalServiceCredentials(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public DirectoryCredentials() {}
|
||||
public ExternalServiceCredentials() {}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
|
@ -0,0 +1,18 @@
|
|||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
public class SecureStorageServiceConfiguration {
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String userAuthenticationTokenSharedSecret;
|
||||
|
||||
public byte[] getUserAuthenticationTokenSharedSecret() throws DecoderException {
|
||||
return Hex.decodeHex(userAuthenticationTokenSharedSecret.toCharArray());
|
||||
}
|
||||
|
||||
}
|
|
@ -26,6 +26,8 @@ import org.slf4j.LoggerFactory;
|
|||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
||||
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.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||
|
@ -34,6 +36,7 @@ import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
|||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceName;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.DeprecatedPin;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
|
@ -95,18 +98,19 @@ public class AccountController {
|
|||
private final Meter captchaFailureMeter = metricRegistry.meter(name(AccountController.class, "captcha_failure" ));
|
||||
|
||||
|
||||
private final PendingAccountsManager pendingAccounts;
|
||||
private final AccountsManager accounts;
|
||||
private final AbusiveHostRules abusiveHostRules;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final SmsSender smsSender;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final MessagesManager messagesManager;
|
||||
private final TurnTokenGenerator turnTokenGenerator;
|
||||
private final Map<String, Integer> testDevices;
|
||||
private final RecaptchaClient recaptchaClient;
|
||||
private final GCMSender gcmSender;
|
||||
private final APNSender apnSender;
|
||||
private final PendingAccountsManager pendingAccounts;
|
||||
private final AccountsManager accounts;
|
||||
private final AbusiveHostRules abusiveHostRules;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final SmsSender smsSender;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final MessagesManager messagesManager;
|
||||
private final TurnTokenGenerator turnTokenGenerator;
|
||||
private final Map<String, Integer> testDevices;
|
||||
private final RecaptchaClient recaptchaClient;
|
||||
private final GCMSender gcmSender;
|
||||
private final APNSender apnSender;
|
||||
private final ExternalServiceCredentialGenerator storageServiceCredentialGenerator;
|
||||
|
||||
public AccountController(PendingAccountsManager pendingAccounts,
|
||||
AccountsManager accounts,
|
||||
|
@ -119,20 +123,22 @@ public class AccountController {
|
|||
Map<String, Integer> testDevices,
|
||||
RecaptchaClient recaptchaClient,
|
||||
GCMSender gcmSender,
|
||||
APNSender apnSender)
|
||||
APNSender apnSender,
|
||||
ExternalServiceCredentialGenerator storageServiceCredentialGenerator)
|
||||
{
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.accounts = accounts;
|
||||
this.abusiveHostRules = abusiveHostRules;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.smsSender = smsSenderFactory;
|
||||
this.directoryQueue = directoryQueue;
|
||||
this.messagesManager = messagesManager;
|
||||
this.testDevices = testDevices;
|
||||
this.turnTokenGenerator = turnTokenGenerator;
|
||||
this.recaptchaClient = recaptchaClient;
|
||||
this.gcmSender = gcmSender;
|
||||
this.apnSender = apnSender;
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.accounts = accounts;
|
||||
this.abusiveHostRules = abusiveHostRules;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.smsSender = smsSenderFactory;
|
||||
this.directoryQueue = directoryQueue;
|
||||
this.messagesManager = messagesManager;
|
||||
this.testDevices = testDevices;
|
||||
this.turnTokenGenerator = turnTokenGenerator;
|
||||
this.recaptchaClient = recaptchaClient;
|
||||
this.gcmSender = gcmSender;
|
||||
this.apnSender = apnSender;
|
||||
this.storageServiceCredentialGenerator = storageServiceCredentialGenerator;
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
@ -260,25 +266,42 @@ public class AccountController {
|
|||
|
||||
Optional<Account> existingAccount = accounts.get(number);
|
||||
|
||||
if (existingAccount.isPresent() &&
|
||||
existingAccount.get().getPin().isPresent() &&
|
||||
if (existingAccount.isPresent() &&
|
||||
(existingAccount.get().getPin().isPresent() || existingAccount.get().getRegistrationLock().isPresent()) &&
|
||||
System.currentTimeMillis() - existingAccount.get().getLastSeen() < TimeUnit.DAYS.toMillis(7))
|
||||
{
|
||||
rateLimiters.getVerifyLimiter().clear(number);
|
||||
|
||||
long timeRemaining = TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - existingAccount.get().getLastSeen());
|
||||
long timeRemaining = TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - existingAccount.get().getLastSeen());
|
||||
Optional<ExternalServiceCredentials> credentials = existingAccount.get().getRegistrationLock().isPresent() &&
|
||||
existingAccount.get().getRegistrationLockSalt().isPresent() ?
|
||||
Optional.of(storageServiceCredentialGenerator.generateFor(number)) :
|
||||
Optional.empty();
|
||||
|
||||
if (accountAttributes.getPin() == null) {
|
||||
if (Util.isEmpty(accountAttributes.getPin()) &&
|
||||
Util.isEmpty(accountAttributes.getRegistrationLock()))
|
||||
{
|
||||
throw new WebApplicationException(Response.status(423)
|
||||
.entity(new RegistrationLockFailure(timeRemaining))
|
||||
.entity(new RegistrationLockFailure(timeRemaining, credentials.orElse(null)))
|
||||
.build());
|
||||
}
|
||||
|
||||
rateLimiters.getPinLimiter().validate(number);
|
||||
|
||||
if (!MessageDigest.isEqual(existingAccount.get().getPin().get().getBytes(), accountAttributes.getPin().getBytes())) {
|
||||
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) {
|
||||
throw new WebApplicationException(Response.status(423)
|
||||
.entity(new RegistrationLockFailure(timeRemaining))
|
||||
.entity(new RegistrationLockFailure(timeRemaining, credentials.orElse(null)))
|
||||
.build());
|
||||
}
|
||||
|
||||
|
@ -382,12 +405,37 @@ public class AccountController {
|
|||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@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.setPin(null);
|
||||
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/registration_lock")
|
||||
public void removeRegistrationLock(@Auth Account account) {
|
||||
account.setRegistrationLock(null);
|
||||
account.setRegistrationLockSalt(null);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/pin/")
|
||||
public void setPin(@Auth Account account, @Valid RegistrationLock accountLock) {
|
||||
public void setPin(@Auth Account account, @Valid DeprecatedPin accountLock) {
|
||||
account.setPin(accountLock.getPin());
|
||||
account.setRegistrationLock(null);
|
||||
account.setRegistrationLockSalt(null);
|
||||
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
|
@ -436,7 +484,18 @@ public class AccountController {
|
|||
device.setSignalingKey(attributes.getSignalingKey());
|
||||
device.setUserAgent(userAgent);
|
||||
|
||||
account.setPin(attributes.getPin());
|
||||
if (!Util.isEmpty(attributes.getPin())) {
|
||||
account.setPin(attributes.getPin());
|
||||
} else if (!Util.isEmpty(attributes.getRegistrationLock())) {
|
||||
AuthenticationCredentials credentials = new AuthenticationCredentials(attributes.getRegistrationLock());
|
||||
account.setRegistrationLock(credentials.getHashedAuthenticationToken());
|
||||
account.setRegistrationLockSalt(credentials.getSalt());
|
||||
} else {
|
||||
account.setPin(null);
|
||||
account.setRegistrationLock(null);
|
||||
account.setRegistrationLockSalt(null);
|
||||
}
|
||||
|
||||
account.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
|
||||
account.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import com.codahale.metrics.SharedMetricRegistries;
|
|||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
|
@ -86,17 +86,17 @@ public class DirectoryController {
|
|||
}
|
||||
}};
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final DirectoryManager directory;
|
||||
private final DirectoryCredentialsGenerator userTokenGenerator;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final DirectoryManager directory;
|
||||
private final ExternalServiceCredentialGenerator directoryServiceTokenGenerator;
|
||||
|
||||
public DirectoryController(RateLimiters rateLimiters,
|
||||
DirectoryManager directory,
|
||||
DirectoryCredentialsGenerator userTokenGenerator)
|
||||
ExternalServiceCredentialGenerator userTokenGenerator)
|
||||
{
|
||||
this.directory = directory;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.userTokenGenerator = userTokenGenerator;
|
||||
this.directory = directory;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.directoryServiceTokenGenerator = userTokenGenerator;
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
@ -104,7 +104,7 @@ public class DirectoryController {
|
|||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getAuthToken(@Auth Account account) {
|
||||
return Response.ok().entity(userTokenGenerator.generateFor(account.getNumber())).build();
|
||||
return Response.ok().entity(directoryServiceTokenGenerator.generateFor(account.getNumber())).build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/storage")
|
||||
public class SecureStorageController {
|
||||
|
||||
private final ExternalServiceCredentialGenerator storageServiceCredentialGenerator;
|
||||
|
||||
public SecureStorageController(ExternalServiceCredentialGenerator storageServiceCredentialGenerator) {
|
||||
this.storageServiceCredentialGenerator = storageServiceCredentialGenerator;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public ExternalServiceCredentials getAuth(@Auth Account account) {
|
||||
return storageServiceCredentialGenerator.generateFor(account.getNumber());
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ package org.whispersystems.textsecuregcm.entities;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
public class AccountAttributes {
|
||||
|
||||
|
@ -36,15 +35,12 @@ public class AccountAttributes {
|
|||
@Length(max = 204, message = "This field must be less than 50 characters")
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private boolean voice;
|
||||
|
||||
@JsonProperty
|
||||
private boolean video;
|
||||
|
||||
@JsonProperty
|
||||
private String pin;
|
||||
|
||||
@JsonProperty
|
||||
private String registrationLock;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] unidentifiedAccessKey;
|
||||
|
||||
|
@ -55,18 +51,17 @@ public class AccountAttributes {
|
|||
|
||||
@VisibleForTesting
|
||||
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String pin) {
|
||||
this(signalingKey, fetchesMessages, registrationId, null, false, false, pin);
|
||||
this(signalingKey, fetchesMessages, registrationId, null, pin, null);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice, boolean video, String pin) {
|
||||
this.signalingKey = signalingKey;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.name = name;
|
||||
this.voice = voice;
|
||||
this.video = video;
|
||||
this.pin = pin;
|
||||
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, String pin, String registrationLock) {
|
||||
this.signalingKey = signalingKey;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.name = name;
|
||||
this.pin = pin;
|
||||
this.registrationLock = registrationLock;
|
||||
}
|
||||
|
||||
public String getSignalingKey() {
|
||||
|
@ -85,18 +80,14 @@ public class AccountAttributes {
|
|||
return name;
|
||||
}
|
||||
|
||||
public boolean getVoice() {
|
||||
return voice;
|
||||
}
|
||||
|
||||
public boolean getVideo() {
|
||||
return video;
|
||||
}
|
||||
|
||||
public String getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public String getRegistrationLock() {
|
||||
return registrationLock;
|
||||
}
|
||||
|
||||
public byte[] getUnidentifiedAccessKey() {
|
||||
return unidentifiedAccessKey;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
public class DeprecatedPin {
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
@Length(min=4,max=20)
|
||||
private String pin;
|
||||
|
||||
public DeprecatedPin() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public DeprecatedPin(String pin) {
|
||||
this.pin = pin;
|
||||
}
|
||||
|
||||
public String getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
}
|
|
@ -8,19 +8,19 @@ import org.hibernate.validator.constraints.NotEmpty;
|
|||
public class RegistrationLock {
|
||||
|
||||
@JsonProperty
|
||||
@Length(min=64,max=64)
|
||||
@NotEmpty
|
||||
@Length(min=4,max=20)
|
||||
private String pin;
|
||||
private String registrationLock;
|
||||
|
||||
public RegistrationLock() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public RegistrationLock(String pin) {
|
||||
this.pin = pin;
|
||||
public RegistrationLock(String registrationLock) {
|
||||
this.registrationLock = registrationLock;
|
||||
}
|
||||
|
||||
public String getPin() {
|
||||
return pin;
|
||||
public String getRegistrationLock() {
|
||||
return registrationLock;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,20 +2,30 @@ package org.whispersystems.textsecuregcm.entities;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
|
||||
public class RegistrationLockFailure {
|
||||
|
||||
@JsonProperty
|
||||
private long timeRemaining;
|
||||
|
||||
@JsonProperty
|
||||
private ExternalServiceCredentials storageCredentials;
|
||||
|
||||
public RegistrationLockFailure() {}
|
||||
|
||||
public RegistrationLockFailure(long timeRemaining) {
|
||||
this.timeRemaining = timeRemaining;
|
||||
public RegistrationLockFailure(long timeRemaining, ExternalServiceCredentials storageCredentials) {
|
||||
this.timeRemaining = timeRemaining;
|
||||
this.storageCredentials = storageCredentials;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public long getTimeRemaining() {
|
||||
return timeRemaining;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public ExternalServiceCredentials getStorageCredentials() {
|
||||
return storageCredentials;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,12 @@ public class Account implements Principal {
|
|||
@JsonProperty
|
||||
private String pin;
|
||||
|
||||
@JsonProperty
|
||||
private String registrationLock;
|
||||
|
||||
@JsonProperty
|
||||
private String registrationLockSalt;
|
||||
|
||||
@JsonProperty("uak")
|
||||
private byte[] unidentifiedAccessKey;
|
||||
|
||||
|
@ -209,6 +215,22 @@ public class Account implements Principal {
|
|||
this.pin = pin;
|
||||
}
|
||||
|
||||
public void setRegistrationLock(String registrationLock) {
|
||||
this.registrationLock = registrationLock;
|
||||
}
|
||||
|
||||
public Optional<String> getRegistrationLock() {
|
||||
return Optional.ofNullable(registrationLock);
|
||||
}
|
||||
|
||||
public void setRegistrationLockSalt(String registrationLockSalt) {
|
||||
this.registrationLockSalt = registrationLockSalt;
|
||||
}
|
||||
|
||||
public Optional<String> getRegistrationLockSalt() {
|
||||
return Optional.ofNullable(registrationLockSalt);
|
||||
}
|
||||
|
||||
public Optional<byte[]> getUnidentifiedAccessKey() {
|
||||
return Optional.ofNullable(unidentifiedAccessKey);
|
||||
}
|
||||
|
@ -238,4 +260,5 @@ public class Account implements Principal {
|
|||
public boolean implies(Subject subject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package org.whispersystems.textsecuregcm.tests.auth;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
public class AuthenticationCredentialsTest {
|
||||
|
||||
@Test
|
||||
public void testCreating() {
|
||||
AuthenticationCredentials credentials = new AuthenticationCredentials("mypassword");
|
||||
assertThat(credentials.getSalt()).isNotEmpty();
|
||||
assertThat(credentials.getHashedAuthenticationToken()).isNotEmpty();
|
||||
assertThat(credentials.getHashedAuthenticationToken().length()).isEqualTo(40);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatching() {
|
||||
AuthenticationCredentials credentials = new AuthenticationCredentials("mypassword");
|
||||
|
||||
AuthenticationCredentials provided = new AuthenticationCredentials(credentials.getHashedAuthenticationToken(), credentials.getSalt());
|
||||
assertThat(provided.verify("mypassword")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMisMatching() {
|
||||
AuthenticationCredentials credentials = new AuthenticationCredentials("mypassword");
|
||||
|
||||
AuthenticationCredentials provided = new AuthenticationCredentials(credentials.getHashedAuthenticationToken(), credentials.getSalt());
|
||||
assertThat(provided.verify("wrong")).isFalse();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.whispersystems.textsecuregcm.tests.auth;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
public class ExternalServiceCredentialsGeneratorTest {
|
||||
|
||||
@Test
|
||||
public void testGenerateDerivedUsername() {
|
||||
ExternalServiceCredentialGenerator generator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], true);
|
||||
ExternalServiceCredentials credentials = generator.generateFor("+14152222222");
|
||||
|
||||
assertThat(credentials.getUsername()).isNotEqualTo("+14152222222");
|
||||
assertThat(credentials.getPassword().startsWith("+14152222222")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateNoDerivedUsername() {
|
||||
ExternalServiceCredentialGenerator generator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false);
|
||||
ExternalServiceCredentials credentials = generator.generateFor("+14152222222");
|
||||
|
||||
assertThat(credentials.getUsername()).isEqualTo("+14152222222");
|
||||
assertThat(credentials.getPassword().startsWith("+14152222222")).isTrue();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import net.sourceforge.argparse4j.inf.Argument;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
|
@ -15,6 +16,7 @@ import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
|||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.DeprecatedPin;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
|
@ -35,12 +37,14 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.Hex;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
|
@ -60,6 +64,7 @@ public class AccountControllerTest {
|
|||
private static final String SENDER_OVER_PIN = "+14154444444";
|
||||
private static final String SENDER_OVER_PREFIX = "+14156666666";
|
||||
private static final String SENDER_PREAUTH = "+14157777777";
|
||||
private static final String SENDER_REG_LOCK = "+14158888888";
|
||||
|
||||
private static final String ABUSIVE_HOST = "192.168.1.1";
|
||||
private static final String RESTRICTED_HOST = "192.168.1.2";
|
||||
|
@ -86,10 +91,14 @@ public class AccountControllerTest {
|
|||
private TimeProvider timeProvider = mock(TimeProvider.class );
|
||||
private TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class);
|
||||
private Account senderPinAccount = mock(Account.class);
|
||||
private Account senderRegLockAccount = mock(Account.class);
|
||||
private RecaptchaClient recaptchaClient = mock(RecaptchaClient.class);
|
||||
private GCMSender gcmSender = mock(GCMSender.class);
|
||||
private APNSender apnSender = mock(APNSender.class);
|
||||
|
||||
private byte[] registration_lock_key = new byte[32];
|
||||
private ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false);
|
||||
|
||||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
|
@ -108,12 +117,16 @@ public class AccountControllerTest {
|
|||
new HashMap<>(),
|
||||
recaptchaClient,
|
||||
gcmSender,
|
||||
apnSender))
|
||||
apnSender,
|
||||
storageCredentialGenerator))
|
||||
.build();
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
new SecureRandom().nextBytes(registration_lock_key);
|
||||
AuthenticationCredentials registrationLockCredentials = new AuthenticationCredentials(Hex.toStringCondensed(registration_lock_key));
|
||||
|
||||
when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter);
|
||||
|
@ -127,13 +140,20 @@ public class AccountControllerTest {
|
|||
when(senderPinAccount.getPin()).thenReturn(Optional.of("31337"));
|
||||
when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis());
|
||||
|
||||
when(senderRegLockAccount.getPin()).thenReturn(Optional.empty());
|
||||
when(senderRegLockAccount.getRegistrationLock()).thenReturn(Optional.of(registrationLockCredentials.getHashedAuthenticationToken()));
|
||||
when(senderRegLockAccount.getRegistrationLockSalt()).thenReturn(Optional.of(registrationLockCredentials.getSalt()));
|
||||
when(senderRegLockAccount.getLastSeen()).thenReturn(System.currentTimeMillis());
|
||||
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null)));
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER_OLD)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31), null)));
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("333333", System.currentTimeMillis(), null)));
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER_REG_LOCK)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null)));
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("444444", System.currentTimeMillis(), null)));
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER_PREAUTH)).thenReturn(Optional.of(new StoredVerificationCode("555555", System.currentTimeMillis(), "validchallenge")));
|
||||
|
||||
when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||
when(accountsManager.get(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount));
|
||||
when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||
when(accountsManager.get(eq(SENDER))).thenReturn(Optional.empty());
|
||||
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.empty());
|
||||
|
@ -502,6 +522,21 @@ public class AccountControllerTest {
|
|||
verify(pinLimiter).validate(eq(SENDER_PIN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyRegistrationLock() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/code/%s", "666666"))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER_REG_LOCK, "bar"))
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null, null, Hex.toStringCondensed(registration_lock_key)),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
verify(pinLimiter).validate(eq(SENDER_REG_LOCK));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyWrongPin() throws Exception {
|
||||
Response response =
|
||||
|
@ -517,6 +552,21 @@ public class AccountControllerTest {
|
|||
verify(pinLimiter).validate(eq(SENDER_PIN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyWrongRegistrationLock() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/code/%s", "666666"))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER_REG_LOCK, "bar"))
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, Hex.toStringCondensed(new byte[32])),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(423);
|
||||
|
||||
verify(pinLimiter).validate(eq(SENDER_REG_LOCK));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyNoPin() throws Exception {
|
||||
Response response =
|
||||
|
@ -530,10 +580,34 @@ public class AccountControllerTest {
|
|||
assertThat(response.getStatus()).isEqualTo(423);
|
||||
|
||||
RegistrationLockFailure failure = response.readEntity(RegistrationLockFailure.class);
|
||||
assertThat(failure.getStorageCredentials()).isNull();
|
||||
|
||||
verifyNoMoreInteractions(pinLimiter);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyNoRegistrationLock() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/code/%s", "666666"))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER_REG_LOCK, "bar"))
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(423);
|
||||
|
||||
RegistrationLockFailure failure = response.readEntity(RegistrationLockFailure.class);
|
||||
assertThat(failure.getStorageCredentials()).isNotNull();
|
||||
assertThat(failure.getStorageCredentials().getUsername()).isEqualTo(SENDER_REG_LOCK);
|
||||
assertThat(failure.getStorageCredentials().getPassword()).isNotEmpty();
|
||||
assertThat(failure.getStorageCredentials().getPassword().startsWith(SENDER_REG_LOCK)).isTrue();
|
||||
assertThat(failure.getTimeRemaining()).isGreaterThan(0);
|
||||
|
||||
verifyNoMoreInteractions(pinLimiter);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testVerifyLimitPin() throws Exception {
|
||||
Response response =
|
||||
|
@ -577,20 +651,47 @@ public class AccountControllerTest {
|
|||
.target("/v1/accounts/pin/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new RegistrationLock("31337")));
|
||||
.put(Entity.json(new DeprecatedPin("31337")));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
verify(AuthHelper.VALID_ACCOUNT).setPin(eq("31337"));
|
||||
verify(AuthHelper.VALID_ACCOUNT).setRegistrationLock(eq(null));
|
||||
verify(AuthHelper.VALID_ACCOUNT).setRegistrationLockSalt(eq(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetRegistrationLock() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/registration_lock/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new RegistrationLock("1234567890123456789012345678901234567890123456789012345678901234")));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
ArgumentCaptor<String> pinCapture = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> pinSaltCapture = ArgumentCaptor.forClass(String.class);
|
||||
|
||||
verify(AuthHelper.VALID_ACCOUNT, times(1)).setPin(eq(null));
|
||||
verify(AuthHelper.VALID_ACCOUNT, times(1)).setRegistrationLock(pinCapture.capture());
|
||||
verify(AuthHelper.VALID_ACCOUNT, times(1)).setRegistrationLockSalt(pinSaltCapture.capture());
|
||||
|
||||
assertThat(pinCapture.getValue()).isNotEmpty();
|
||||
assertThat(pinSaltCapture.getValue()).isNotEmpty();
|
||||
|
||||
assertThat(pinCapture.getValue().length()).isEqualTo(40);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSetPinUnauthorized() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/pin/")
|
||||
.request()
|
||||
.put(Entity.json(new RegistrationLock("31337")));
|
||||
.put(Entity.json(new DeprecatedPin("31337")));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
@ -602,13 +703,24 @@ public class AccountControllerTest {
|
|||
.target("/v1/accounts/pin/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new DeprecatedPin("313")));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetShortRegistrationLock() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/registration_lock/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(new RegistrationLock("313")));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
|
||||
verify(AuthHelper.VALID_ACCOUNT, never()).setPin(anyString());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSetPinDisabled() throws Exception {
|
||||
Response response =
|
||||
|
@ -616,11 +728,21 @@ public class AccountControllerTest {
|
|||
.target("/v1/accounts/pin/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_NUMBER, AuthHelper.DISABLED_PASSWORD))
|
||||
.put(Entity.json(new RegistrationLock("31337")));
|
||||
.put(Entity.json(new DeprecatedPin("31337")));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
verify(AuthHelper.VALID_ACCOUNT, never()).setPin(anyString());
|
||||
@Test
|
||||
public void testSetRegistrationLockDisabled() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/registration_lock/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_NUMBER, AuthHelper.DISABLED_PASSWORD))
|
||||
.put(Entity.json(new RegistrationLock("1234567890123456789012345678901234567890123456789012345678901234")));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -212,7 +212,7 @@ public class DeviceControllerTest {
|
|||
.target("/v1/devices/5678901")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters it's so long that it's even longer than 204 characters. that's a lot of characters. we're talking lots and lots and lots of characters. 12345678", true, true, null),
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters it's so long that it's even longer than 204 characters. that's a lot of characters. we're talking lots and lots and lots of characters. 12345678", null, null),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertEquals(response.getStatus(), 422);
|
||||
|
|
|
@ -7,8 +7,8 @@ import org.junit.Rule;
|
|||
import org.junit.Test;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.whispersystems.textsecuregcm.auth.DirectoryCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
|
||||
|
@ -42,9 +42,9 @@ public class DirectoryControllerTest {
|
|||
private final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private final RateLimiter rateLimiter = mock(RateLimiter.class);
|
||||
private final DirectoryManager directoryManager = mock(DirectoryManager.class);
|
||||
private final DirectoryCredentialsGenerator directoryCredentialsGenerator = mock(DirectoryCredentialsGenerator.class);
|
||||
private final ExternalServiceCredentialGenerator directoryCredentialsGenerator = mock(ExternalServiceCredentialGenerator.class);
|
||||
|
||||
private final DirectoryCredentials validCredentials = new DirectoryCredentials("username", "password");
|
||||
private final ExternalServiceCredentials validCredentials = new ExternalServiceCredentials("username", "password");
|
||||
|
||||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
|
@ -140,12 +140,12 @@ public class DirectoryControllerTest {
|
|||
|
||||
@Test
|
||||
public void testGetAuthToken() {
|
||||
DirectoryCredentials token =
|
||||
ExternalServiceCredentials token =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/directory/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.get(DirectoryCredentials.class);
|
||||
.get(ExternalServiceCredentials.class);
|
||||
assertThat(token.getUsername()).isEqualTo(validCredentials.getUsername());
|
||||
assertThat(token.getPassword()).isEqualTo(validCredentials.getPassword());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
public class SecureStorageControllerTest {
|
||||
|
||||
private static final ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false);
|
||||
|
||||
@ClassRule
|
||||
public static final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)))
|
||||
.setMapper(SystemMapper.getMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new SecureStorageController(storageCredentialGenerator))
|
||||
.build();
|
||||
|
||||
|
||||
@Test
|
||||
public void testGetCredentials() throws Exception {
|
||||
ExternalServiceCredentials credentials = resources.getJerseyTest()
|
||||
.target("/v1/storage/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.get(ExternalServiceCredentials.class);
|
||||
|
||||
assertThat(credentials.getPassword()).isNotEmpty();
|
||||
assertThat(credentials.getUsername()).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCredentialsBadAuth() throws Exception {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/storage/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.INVVALID_NUMBER, AuthHelper.INVALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1 +1 @@
|
|||
{"devices":[{"id":1,"name":"foo","authToken":"bar","salt":"salt","signalingKey":"keykey","gcmId":"gcm-id","apnId":"apn-id","voipApnId":"voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"identity_key_value","name":"OneProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}
|
||||
{"devices":[{"id":1,"name":"foo","authToken":"bar","salt":"salt","signalingKey":"keykey","gcmId":"gcm-id","apnId":"apn-id","voipApnId":"voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"identity_key_value","name":"OneProfileName","avatar":null,"avatarDigest":null,"pin":"******","registrationLock":null, "registrationLockSalt":null,"uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"devices":[{"id":1,"name":"2foo","authToken":"2bar","salt":"2salt","signalingKey":"2keykey","gcmId":"2gcm-id","apnId":"2apn-id","voipApnId":"2voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"different_identity_key_value","name":"TwoProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}
|
||||
{"devices":[{"id":1,"name":"2foo","authToken":"2bar","salt":"2salt","signalingKey":"2keykey","gcmId":"2gcm-id","apnId":"2apn-id","voipApnId":"2voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"different_identity_key_value","name":"TwoProfileName","avatar":null,"avatarDigest":null,"pin":"******","registrationLock":null,"registrationLockSalt":null,"uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}
|
||||
|
|
Loading…
Reference in New Issue