diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticationCredentials.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticationCredentials.java deleted file mode 100644 index 88edd0c32..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticationCredentials.java +++ /dev/null @@ -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); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java index 6cbb02605..9ca61e2a7 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java @@ -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))); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/SaltedTokenHash.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/SaltedTokenHash.java new file mode 100644 index 000000000..c97d78451 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/SaltedTokenHash.java @@ -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); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLock.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLock.java index bff1462fa..96f8f7057 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLock.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLock.java @@ -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; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index 8e72c4d70..0787520f2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -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 diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java index 87299577d..fbce3c8e1 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java @@ -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); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 24cb27c29..2192c5c67 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -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() { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java index cc88f3fc5..8dd08df90 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -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() { @Override public void accept(final Device device) { - device.setAuthenticationCredentials(credentials); + device.setAuthTokenHash(credentials); } }); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java index 440493ef8..e0c8c9b10 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java @@ -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 diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java index e369b531b..fb9a2e8c2 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 Signal Messenger, LLC + * Copyright 2013 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ @@ -155,7 +155,7 @@ class BaseAccountAuthenticatorTest { final Account account = mock(Account.class); final Device device = mock(Device.class); - final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); @@ -164,9 +164,9 @@ class BaseAccountAuthenticatorTest { when(account.isEnabled()).thenReturn(true); when(device.getId()).thenReturn(deviceId); when(device.isEnabled()).thenReturn(true); - when(device.getAuthenticationCredentials()).thenReturn(credentials); + when(device.getAuthTokenHash()).thenReturn(credentials); when(credentials.verify(password)).thenReturn(true); - when(credentials.getVersion()).thenReturn(AuthenticationCredentials.CURRENT_VERSION); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); final Optional maybeAuthenticatedAccount = baseAccountAuthenticator.authenticate(new BasicCredentials(uuid.toString(), password), true); @@ -185,7 +185,7 @@ class BaseAccountAuthenticatorTest { final Account account = mock(Account.class); final Device device = mock(Device.class); - final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); @@ -194,9 +194,9 @@ class BaseAccountAuthenticatorTest { when(account.isEnabled()).thenReturn(true); when(device.getId()).thenReturn(deviceId); when(device.isEnabled()).thenReturn(true); - when(device.getAuthenticationCredentials()).thenReturn(credentials); + when(device.getAuthTokenHash()).thenReturn(credentials); when(credentials.verify(password)).thenReturn(true); - when(credentials.getVersion()).thenReturn(AuthenticationCredentials.CURRENT_VERSION); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); final Optional maybeAuthenticatedAccount = baseAccountAuthenticator.authenticate(new BasicCredentials(uuid + "." + deviceId, password), true); @@ -219,7 +219,7 @@ class BaseAccountAuthenticatorTest { final Account account = mock(Account.class); final Device authenticatedDevice = mock(Device.class); - final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); @@ -228,9 +228,9 @@ class BaseAccountAuthenticatorTest { when(account.isEnabled()).thenReturn(accountEnabled); when(authenticatedDevice.getId()).thenReturn(deviceId); when(authenticatedDevice.isEnabled()).thenReturn(deviceEnabled); - when(authenticatedDevice.getAuthenticationCredentials()).thenReturn(credentials); + when(authenticatedDevice.getAuthTokenHash()).thenReturn(credentials); when(credentials.verify(password)).thenReturn(true); - when(credentials.getVersion()).thenReturn(AuthenticationCredentials.CURRENT_VERSION); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); final String identifier; if (authenticatedDeviceIsPrimary) { @@ -258,7 +258,7 @@ class BaseAccountAuthenticatorTest { final Account account = mock(Account.class); final Device device = mock(Device.class); - final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); @@ -267,9 +267,9 @@ class BaseAccountAuthenticatorTest { when(account.isEnabled()).thenReturn(true); when(device.getId()).thenReturn(deviceId); when(device.isEnabled()).thenReturn(true); - when(device.getAuthenticationCredentials()).thenReturn(credentials); + when(device.getAuthTokenHash()).thenReturn(credentials); when(credentials.verify(password)).thenReturn(true); - when(credentials.getVersion()).thenReturn(AuthenticationCredentials.Version.V1); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.Version.V1); final Optional maybeAuthenticatedAccount = baseAccountAuthenticator.authenticate(new BasicCredentials(uuid.toString(), password), true); @@ -295,7 +295,7 @@ class BaseAccountAuthenticatorTest { final Account account = mock(Account.class); final Device device = mock(Device.class); - final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); @@ -304,9 +304,9 @@ class BaseAccountAuthenticatorTest { when(account.isEnabled()).thenReturn(true); when(device.getId()).thenReturn(deviceId); when(device.isEnabled()).thenReturn(true); - when(device.getAuthenticationCredentials()).thenReturn(credentials); + when(device.getAuthTokenHash()).thenReturn(credentials); when(credentials.verify(password)).thenReturn(true); - when(credentials.getVersion()).thenReturn(AuthenticationCredentials.CURRENT_VERSION); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); final Optional maybeAuthenticatedAccount = baseAccountAuthenticator.authenticate(new BasicCredentials(uuid + "." + (deviceId + 1), password), true); @@ -323,7 +323,7 @@ class BaseAccountAuthenticatorTest { final Account account = mock(Account.class); final Device device = mock(Device.class); - final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); clock.unpin(); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); @@ -332,9 +332,9 @@ class BaseAccountAuthenticatorTest { when(account.isEnabled()).thenReturn(true); when(device.getId()).thenReturn(deviceId); when(device.isEnabled()).thenReturn(true); - when(device.getAuthenticationCredentials()).thenReturn(credentials); + when(device.getAuthTokenHash()).thenReturn(credentials); when(credentials.verify(password)).thenReturn(true); - when(credentials.getVersion()).thenReturn(AuthenticationCredentials.CURRENT_VERSION); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); final String incorrectPassword = password + "incorrect"; diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java index 3bbfc2682..57925e3a0 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java @@ -63,9 +63,9 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; -import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; 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.TurnTokenGenerator; @@ -213,7 +213,7 @@ class AccountControllerTest { clearInvocations(AuthHelper.VALID_ACCOUNT, AuthHelper.UNDISCOVERABLE_ACCOUNT); new SecureRandom().nextBytes(registration_lock_key); - AuthenticationCredentials registrationLockCredentials = new AuthenticationCredentials(Hex.toStringCondensed(registration_lock_key)); + SaltedTokenHash registrationLockCredentials = SaltedTokenHash.generateFor(Hex.toStringCondensed(registration_lock_key)); AccountsHelper.setupMockUpdate(accountsManager); @@ -235,7 +235,7 @@ class AccountControllerTest { when(senderHasStorage.isStorageSupported()).thenReturn(true); when(senderHasStorage.getRegistrationLock()).thenReturn(new StoredRegistrationLock(Optional.empty(), Optional.empty(), System.currentTimeMillis())); - when(senderRegLockAccount.getRegistrationLock()).thenReturn(new StoredRegistrationLock(Optional.of(registrationLockCredentials.getHashedAuthenticationToken()), Optional.of(registrationLockCredentials.getSalt()), System.currentTimeMillis())); + when(senderRegLockAccount.getRegistrationLock()).thenReturn(new StoredRegistrationLock(Optional.of(registrationLockCredentials.hash()), Optional.of(registrationLockCredentials.salt()), System.currentTimeMillis())); when(senderRegLockAccount.getLastSeen()).thenReturn(System.currentTimeMillis()); when(senderRegLockAccount.getUuid()).thenReturn(SENDER_REG_LOCK_UUID); when(senderRegLockAccount.getNumber()).thenReturn(SENDER_REG_LOCK); @@ -1483,7 +1483,7 @@ class AccountControllerTest { MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(200); - verify(senderRegLockAccount, never()).lockAuthenticationCredentials(); + verify(senderRegLockAccount, never()).lockAuthTokenHash(); verify(clientPresenceManager, never()).disconnectAllPresences(eq(SENDER_REG_LOCK_UUID), any()); verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any(), any(), any()); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java index af685a9c8..e065b803e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 Signal Messenger, LLC + * Copyright 2013 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ @@ -38,7 +38,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; -import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.SignedPreKey; @@ -199,7 +199,7 @@ class AccountsManagerConcurrentModificationIntegrationTest { final byte[] unidentifiedAccessKey = new byte[]{1}; final String pin = "1234"; final String registrationLock = "reglock"; - final AuthenticationCredentials credentials = new AuthenticationCredentials(registrationLock); + final SaltedTokenHash credentials = SaltedTokenHash.generateFor(registrationLock); final boolean unrestrictedUnidentifiedAccess = true; final long lastSeen = Instant.now().getEpochSecond(); @@ -208,7 +208,7 @@ class AccountsManagerConcurrentModificationIntegrationTest { modifyAccount(uuid, account -> account.setCurrentProfileVersion(currentProfileVersion)), modifyAccount(uuid, account -> account.setIdentityKey(identityKey)), modifyAccount(uuid, account -> account.setUnidentifiedAccessKey(unidentifiedAccessKey)), - modifyAccount(uuid, account -> account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt())), + modifyAccount(uuid, account -> account.setRegistrationLock(credentials.hash(), credentials.salt())), modifyAccount(uuid, account -> account.setUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedAccess)), modifyDevice(uuid, Device.MASTER_ID, device -> device.setLastSeen(lastSeen)), modifyDevice(uuid, Device.MASTER_ID, device -> device.setName("deviceName")) diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/AuthenticationCredentialsTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/AuthenticationCredentialsTest.java deleted file mode 100644 index 37970fa1b..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/AuthenticationCredentialsTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.tests.auth; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -import org.junit.jupiter.api.Test; -import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; - -class AuthenticationCredentialsTest { - - @Test - void testCreating() { - AuthenticationCredentials credentials = new AuthenticationCredentials("mypassword"); - assertThat(credentials.getSalt()).isNotEmpty(); - assertThat(credentials.getHashedAuthenticationToken()).isNotEmpty(); - assertThat(credentials.getHashedAuthenticationToken().length()).isEqualTo(66); - } - - @Test - void testMatching() { - AuthenticationCredentials credentials = new AuthenticationCredentials("mypassword"); - - AuthenticationCredentials provided = new AuthenticationCredentials(credentials.getHashedAuthenticationToken(), credentials.getSalt()); - assertThat(provided.verify("mypassword")).isTrue(); - } - - @Test - void testMisMatching() { - AuthenticationCredentials credentials = new AuthenticationCredentials("mypassword"); - - AuthenticationCredentials provided = new AuthenticationCredentials(credentials.getHashedAuthenticationToken(), credentials.getSalt()); - assertThat(provided.verify("wrong")).isFalse(); - } - -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/SaltedTokenHashTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/SaltedTokenHashTest.java new file mode 100644 index 000000000..55f83e5d0 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/SaltedTokenHashTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.auth; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; + +class SaltedTokenHashTest { + + @Test + void testCreating() { + SaltedTokenHash credentials = SaltedTokenHash.generateFor("mypassword"); + assertThat(credentials.salt()).isNotEmpty(); + assertThat(credentials.hash()).isNotEmpty(); + assertThat(credentials.hash().length()).isEqualTo(66); + } + + @Test + void testMatching() { + SaltedTokenHash credentials = SaltedTokenHash.generateFor("mypassword"); + + SaltedTokenHash provided = new SaltedTokenHash(credentials.hash(), credentials.salt()); + assertThat(provided.verify("mypassword")).isTrue(); + } + + @Test + void testMisMatching() { + SaltedTokenHash credentials = SaltedTokenHash.generateFor("mypassword"); + + SaltedTokenHash provided = new SaltedTokenHash(credentials.hash(), credentials.salt()); + assertThat(provided.verify("wrong")).isFalse(); + } + +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java index 42083387f..622112e0a 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 Signal Messenger, LLC + * Copyright 2013 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ @@ -20,7 +20,7 @@ import java.util.UUID; import java.util.function.Consumer; import org.mockito.MockingDetails; import org.mockito.stubbing.Stubbing; -import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; @@ -75,7 +75,7 @@ public class AccountsHelper { }); when(mockAccountsManager.updateDeviceAuthentication(any(), any(), any())).thenAnswer(answer -> { - answer.getArgument(1, Device.class).setAuthenticationCredentials(answer.getArgument(2, AuthenticationCredentials.class)); + answer.getArgument(1, Device.class).setAuthTokenHash(answer.getArgument(2, SaltedTokenHash.class)); return mockAccountsManager.update(answer.getArgument(0, Account.class), account -> {}); }); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java index 4d75d06f6..5f174daf4 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 Signal Messenger, LLC + * Copyright 2013 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ @@ -22,9 +22,9 @@ import java.util.Random; import java.util.UUID; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; -import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; @@ -79,12 +79,12 @@ public class AuthHelper { public static Device VALID_DEVICE_3_PRIMARY = mock(Device.class); public static Device VALID_DEVICE_3_LINKED = mock(Device.class); - private static AuthenticationCredentials VALID_CREDENTIALS = mock(AuthenticationCredentials.class); - private static AuthenticationCredentials VALID_CREDENTIALS_TWO = mock(AuthenticationCredentials.class); - private static AuthenticationCredentials VALID_CREDENTIALS_3_PRIMARY = mock(AuthenticationCredentials.class); - private static AuthenticationCredentials VALID_CREDENTIALS_3_LINKED = mock(AuthenticationCredentials.class); - private static AuthenticationCredentials DISABLED_CREDENTIALS = mock(AuthenticationCredentials.class); - private static AuthenticationCredentials UNDISCOVERABLE_CREDENTIALS = mock(AuthenticationCredentials.class); + private static SaltedTokenHash VALID_CREDENTIALS = mock(SaltedTokenHash.class); + private static SaltedTokenHash VALID_CREDENTIALS_TWO = mock(SaltedTokenHash.class); + private static SaltedTokenHash VALID_CREDENTIALS_3_PRIMARY = mock(SaltedTokenHash.class); + private static SaltedTokenHash VALID_CREDENTIALS_3_LINKED = mock(SaltedTokenHash.class); + private static SaltedTokenHash DISABLED_CREDENTIALS = mock(SaltedTokenHash.class); + private static SaltedTokenHash UNDISCOVERABLE_CREDENTIALS = mock(SaltedTokenHash.class); public static PolymorphicAuthDynamicFeature getAuthFilter() { when(VALID_CREDENTIALS.verify("foo")).thenReturn(true); @@ -94,12 +94,12 @@ public class AuthHelper { when(DISABLED_CREDENTIALS.verify(DISABLED_PASSWORD)).thenReturn(true); when(UNDISCOVERABLE_CREDENTIALS.verify(UNDISCOVERABLE_PASSWORD)).thenReturn(true); - when(VALID_DEVICE.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS); - when(VALID_DEVICE_TWO.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS_TWO); - when(VALID_DEVICE_3_PRIMARY.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS_3_PRIMARY); - when(VALID_DEVICE_3_LINKED.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS_3_LINKED); - when(DISABLED_DEVICE.getAuthenticationCredentials()).thenReturn(DISABLED_CREDENTIALS); - when(UNDISCOVERABLE_DEVICE.getAuthenticationCredentials()).thenReturn(UNDISCOVERABLE_CREDENTIALS); + when(VALID_DEVICE.getAuthTokenHash()).thenReturn(VALID_CREDENTIALS); + when(VALID_DEVICE_TWO.getAuthTokenHash()).thenReturn(VALID_CREDENTIALS_TWO); + when(VALID_DEVICE_3_PRIMARY.getAuthTokenHash()).thenReturn(VALID_CREDENTIALS_3_PRIMARY); + when(VALID_DEVICE_3_LINKED.getAuthTokenHash()).thenReturn(VALID_CREDENTIALS_3_LINKED); + when(DISABLED_DEVICE.getAuthTokenHash()).thenReturn(DISABLED_CREDENTIALS); + when(UNDISCOVERABLE_DEVICE.getAuthTokenHash()).thenReturn(UNDISCOVERABLE_CREDENTIALS); when(VALID_DEVICE.isMaster()).thenReturn(true); when(VALID_DEVICE_TWO.isMaster()).thenReturn(true); @@ -231,7 +231,7 @@ public class AuthHelper { public final String password; public final Account account = mock(Account.class); public final Device device = mock(Device.class); - public final AuthenticationCredentials authenticationCredentials = mock(AuthenticationCredentials.class); + public final SaltedTokenHash saltedTokenHash = mock(SaltedTokenHash.class); public TestAccount(String number, UUID uuid, String password) { this.number = number; @@ -244,8 +244,8 @@ public class AuthHelper { } private void setup(final AccountsManager accountsManager) { - when(authenticationCredentials.verify(password)).thenReturn(true); - when(device.getAuthenticationCredentials()).thenReturn(authenticationCredentials); + when(saltedTokenHash.verify(password)).thenReturn(true); + when(device.getAuthTokenHash()).thenReturn(saltedTokenHash); when(device.isMaster()).thenReturn(true); when(device.getId()).thenReturn(1L); when(device.isEnabled()).thenReturn(true);