AuthenticationCredentials name changed to SaltedTokenHash

This commit is contained in:
Sergey Skrobotov
2023-01-29 23:32:43 -08:00
parent dc8f62a4ad
commit 8d0e23bde1
16 changed files with 203 additions and 215 deletions

View File

@@ -1,86 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.codec.binary.Hex;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.whispersystems.textsecuregcm.util.Util;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class AuthenticationCredentials {
private static final String V2_PREFIX = "2.";
private final String hashedAuthenticationToken;
private final String salt;
public enum Version {
V1,
V2,
}
public static final Version CURRENT_VERSION = Version.V2;
public AuthenticationCredentials(String hashedAuthenticationToken, String salt) {
this.hashedAuthenticationToken = hashedAuthenticationToken;
this.salt = salt;
}
public AuthenticationCredentials(String authenticationToken) {
this.salt = String.valueOf(Util.ensureNonNegativeInt(new SecureRandom().nextInt()));
this.hashedAuthenticationToken = getV2HashedValue(salt, authenticationToken);
}
@VisibleForTesting
public AuthenticationCredentials v1ForTesting(String authenticationToken) {
String salt = String.valueOf(Util.ensureNonNegativeInt(new SecureRandom().nextInt()));
return new AuthenticationCredentials(getV1HashedValue(salt, authenticationToken), salt);
}
public Version getVersion() {
if (this.hashedAuthenticationToken.startsWith(V2_PREFIX)) {
return Version.V2;
}
return Version.V1;
}
public String getHashedAuthenticationToken() {
return hashedAuthenticationToken;
}
public String getSalt() {
return salt;
}
public boolean verify(String authenticationToken) {
final String theirValue = switch (getVersion()) {
case V1 -> getV1HashedValue(salt, authenticationToken);
case V2 -> getV2HashedValue(salt, authenticationToken);
};
return MessageDigest.isEqual(theirValue.getBytes(StandardCharsets.UTF_8), this.hashedAuthenticationToken.getBytes(StandardCharsets.UTF_8));
}
private static String getV1HashedValue(String salt, String token) {
try {
return new String(Hex.encodeHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes(StandardCharsets.UTF_8))));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private static final byte[] AUTH_TOKEN_HKDF_INFO = "authtoken".getBytes(StandardCharsets.UTF_8);
private static String getV2HashedValue(String salt, String token) {
byte[] secret = HKDF.deriveSecrets(
token.getBytes(StandardCharsets.UTF_8), // key
salt.getBytes(StandardCharsets.UTF_8), // salt
AUTH_TOKEN_HKDF_INFO,
32);
return V2_PREFIX + Hex.encodeHexString(secret);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -123,15 +123,15 @@ public class BaseAccountAuthenticator {
.increment();
}
AuthenticationCredentials deviceAuthenticationCredentials = device.get().getAuthenticationCredentials();
if (deviceAuthenticationCredentials.verify(basicCredentials.getPassword())) {
SaltedTokenHash deviceSaltedTokenHash = device.get().getAuthTokenHash();
if (deviceSaltedTokenHash.verify(basicCredentials.getPassword())) {
succeeded = true;
Account authenticatedAccount = updateLastSeen(account.get(), device.get());
if (deviceAuthenticationCredentials.getVersion() != AuthenticationCredentials.CURRENT_VERSION) {
if (deviceSaltedTokenHash.getVersion() != SaltedTokenHash.CURRENT_VERSION) {
authenticatedAccount = accountsManager.updateDeviceAuthentication(
authenticatedAccount,
device.get(),
new AuthenticationCredentials(basicCredentials.getPassword())); // new credentials have current version
SaltedTokenHash.generateFor(basicCredentials.getPassword())); // new credentials have current version
}
return Optional.of(new AuthenticatedAccount(
new RefreshingAccountAndDeviceSupplier(authenticatedAccount, device.get().getId(), accountsManager)));

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import org.apache.commons.codec.binary.Hex;
import org.signal.libsignal.protocol.kdf.HKDF;
public record SaltedTokenHash(String hash, String salt) {
public enum Version {
V1,
V2,
}
public static final Version CURRENT_VERSION = Version.V2;
private static final String V2_PREFIX = "2.";
private static final byte[] AUTH_TOKEN_HKDF_INFO = "authtoken".getBytes(StandardCharsets.UTF_8);
private static final int SALT_SIZE = 16;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static SaltedTokenHash generateFor(final String token) {
final String salt = generateSalt();
final String hash = calculateV2Hash(salt, token);
return new SaltedTokenHash(hash, salt);
}
public Version getVersion() {
return hash.startsWith(V2_PREFIX) ? Version.V2 : Version.V1;
}
public boolean verify(final String token) {
final String theirValue = switch (getVersion()) {
case V1 -> calculateV1Hash(salt, token);
case V2 -> calculateV2Hash(salt, token);
};
return MessageDigest.isEqual(
theirValue.getBytes(StandardCharsets.UTF_8),
hash.getBytes(StandardCharsets.UTF_8));
}
private static String generateSalt() {
final byte[] salt = new byte[SALT_SIZE];
SECURE_RANDOM.nextBytes(salt);
return Hex.encodeHexString(salt);
}
private static String calculateV1Hash(final String salt, final String token) {
try {
return new String(
Hex.encodeHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes(StandardCharsets.UTF_8))));
} catch (final NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private static String calculateV2Hash(final String salt, final String token) {
final byte[] secret = HKDF.deriveSecrets(
token.getBytes(StandardCharsets.UTF_8), // key
salt.getBytes(StandardCharsets.UTF_8), // salt
AUTH_TOKEN_HKDF_INFO,
32);
return V2_PREFIX + Hex.encodeHexString(secret);
}
}

View File

@@ -1,16 +1,15 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.util.Util;
import javax.annotation.Nullable;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.util.Util;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class StoredRegistrationLock {
@@ -56,7 +55,7 @@ public class StoredRegistrationLock {
public boolean verify(@Nullable String clientRegistrationLock) {
if (hasLockAndSalt() && Util.nonEmpty(clientRegistrationLock)) {
AuthenticationCredentials credentials = new AuthenticationCredentials(registrationLock.get(), registrationLockSalt.get());
SaltedTokenHash credentials = new SaltedTokenHash(registrationLock.get(), registrationLockSalt.get());
return credentials.verify(clientRegistrationLock);
} else {
return false;

View File

@@ -55,14 +55,13 @@ import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.spam.FilterSpam;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.auth.TurnToken;
@@ -97,6 +96,7 @@ import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.registration.ClientType;
import org.whispersystems.textsecuregcm.registration.MessageTransport;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.spam.FilterSpam;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
@@ -615,10 +615,10 @@ public class AccountController {
@Produces(MediaType.APPLICATION_JSON)
@Path("/registration_lock")
public void setRegistrationLock(@Auth AuthenticatedAccount auth, @NotNull @Valid RegistrationLock accountLock) {
AuthenticationCredentials credentials = new AuthenticationCredentials(accountLock.getRegistrationLock());
SaltedTokenHash credentials = SaltedTokenHash.generateFor(accountLock.getRegistrationLock());
accounts.update(auth.getAccount(),
a -> a.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt()));
a -> a.setRegistrationLock(credentials.hash(), credentials.salt()));
}
@Timed

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
@@ -30,9 +30,9 @@ import javax.ws.rs.core.Response;
import org.glassfish.jersey.server.ContainerRequest;
import org.whispersystems.textsecuregcm.auth.AuthEnablementRefreshRequirementProvider;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
@@ -192,7 +192,7 @@ public class DeviceController {
Device device = new Device();
device.setName(accountAttributes.getName());
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setAuthTokenHash(SaltedTokenHash.generateFor(password));
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
accountAttributes.getPhoneNumberIdentityRegistrationId().ifPresent(device::setPhoneNumberIdentityRegistrationId);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
@@ -18,7 +18,7 @@ import java.util.function.Predicate;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
@@ -390,8 +390,8 @@ public class Account {
public void setRegistrationLockFromAttributes(final AccountAttributes attributes) {
if (!Util.isEmpty(attributes.getRegistrationLock())) {
AuthenticationCredentials credentials = new AuthenticationCredentials(attributes.getRegistrationLock());
setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
SaltedTokenHash credentials = SaltedTokenHash.generateFor(attributes.getRegistrationLock());
setRegistrationLock(credentials.hash(), credentials.salt());
} else {
setRegistrationLock(null, null);
}
@@ -485,8 +485,8 @@ public class Account {
* of the phone number, or after 7 days the phone number holder can register a new
* account.
*/
public void lockAuthenticationCredentials() {
devices.forEach(Device::lockAuthenticationCredentials);
public void lockAuthTokenHash() {
devices.forEach(Device::lockAuthTokenHash);
}
boolean isStale() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
@@ -36,7 +36,7 @@ import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
@@ -169,7 +169,7 @@ public class AccountsManager {
deletedAccountsManager.lockAndTake(number, maybeRecentlyDeletedUuid -> {
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setAuthTokenHash(SaltedTokenHash.generateFor(password));
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
accountAttributes.getPhoneNumberIdentityRegistrationId().ifPresent(device::setPhoneNumberIdentityRegistrationId);
@@ -496,12 +496,12 @@ public class AccountsManager {
});
}
public Account updateDeviceAuthentication(final Account account, final Device device, final AuthenticationCredentials credentials) {
Preconditions.checkArgument(credentials.getVersion() == AuthenticationCredentials.CURRENT_VERSION);
public Account updateDeviceAuthentication(final Account account, final Device device, final SaltedTokenHash credentials) {
Preconditions.checkArgument(credentials.getVersion() == SaltedTokenHash.CURRENT_VERSION);
return updateDevice(account, device.getId(), new Consumer<Device>() {
@Override
public void accept(final Device device) {
device.setAuthenticationCredentials(credentials);
device.setAuthTokenHash(credentials);
}
});
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
@@ -9,7 +9,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.OptionalInt;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.Util;
@@ -144,9 +144,9 @@ public class Device {
this.name = name;
}
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.authToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt();
public void setAuthTokenHash(SaltedTokenHash credentials) {
this.authToken = credentials.hash();
this.salt = credentials.salt();
}
/**
@@ -158,8 +158,8 @@ public class Device {
* @return true if the credential was locked, false otherwise.
*/
public boolean hasLockedCredentials() {
AuthenticationCredentials auth = getAuthenticationCredentials();
return auth.getHashedAuthenticationToken().startsWith("!");
SaltedTokenHash auth = getAuthTokenHash();
return auth.hash().startsWith("!");
}
/**
@@ -169,15 +169,15 @@ public class Device {
*
* See that method for more information.
*/
public void lockAuthenticationCredentials() {
AuthenticationCredentials oldAuth = getAuthenticationCredentials();
String token = "!" + oldAuth.getHashedAuthenticationToken();
String salt = oldAuth.getSalt();
setAuthenticationCredentials(new AuthenticationCredentials(token, salt));
public void lockAuthTokenHash() {
SaltedTokenHash oldAuth = getAuthTokenHash();
String token = "!" + oldAuth.hash();
String salt = oldAuth.salt();
setAuthTokenHash(new SaltedTokenHash(token, salt));
}
public AuthenticationCredentials getAuthenticationCredentials() {
return new AuthenticationCredentials(authToken, salt);
public SaltedTokenHash getAuthTokenHash() {
return new SaltedTokenHash(authToken, salt);
}
@Nullable