From acb6510312217b56880c4a0fdc9fbf95ed668ff2 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 10 May 2023 15:53:31 -0400 Subject: [PATCH] Switch to libsignal for PIN hashing. --- app/build.gradle | 6 -- .../app/account/AccountSettingsFragment.kt | 4 +- .../ChangeNumberRegistrationLockFragment.kt | 4 +- .../securesms/keyvalue/KbsValues.java | 4 +- .../securesms/lock/PinHashing.java | 64 ---------------- .../lock/SignalPinReminderDialog.java | 5 +- .../lock/v2/CreateKbsPinViewModel.java | 2 +- .../securesms/pin/KbsRepository.java | 6 +- .../thoughtcrime/securesms/pin/PinState.java | 16 ++-- .../viewmodel/RegistrationViewModel.java | 4 +- ...sDataTest.java => PinHashKbsDataTest.java} | 29 +++++-- ...t.java => PinHashUtil_normalize_Test.java} | 6 +- .../v2/PinValidityChecker_validity_Test.java | 2 +- dependencies.gradle | 1 - .../signalservice/api/KeyBackupService.java | 19 ++--- .../signalservice/api/kbs/HashedPin.java | 51 ------------- .../signalservice/api/kbs/KbsData.java | 2 +- .../signalservice/api/kbs/PinHashUtil.kt | 75 +++++++++++++++++++ .../kbs}/PinString.java | 7 +- .../kbs}/PinValidityChecker.java | 7 +- .../internal/registrationpin/PinHasher.java | 29 ------- 21 files changed, 148 insertions(+), 195 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/PinHashing.java rename app/src/test/java/org/thoughtcrime/securesms/registration/v2/{HashedPinKbsDataTest.java => PinHashKbsDataTest.java} (67%) rename app/src/test/java/org/thoughtcrime/securesms/registration/v2/{PinHasher_normalize_Test.java => PinHashUtil_normalize_Test.java} (87%) delete mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt rename libsignal/service/src/main/java/org/whispersystems/signalservice/{internal/registrationpin => api/kbs}/PinString.java (81%) rename libsignal/service/src/main/java/org/whispersystems/signalservice/{internal/registrationpin => api/kbs}/PinValidityChecker.java (90%) delete mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinHasher.java diff --git a/app/build.gradle b/app/build.gradle index ac7e1034eb..d3bc5f6f91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -520,12 +520,6 @@ dependencies { exclude group: 'com.google.protobuf' } - implementation(libs.signal.argon2) { - artifact { - type = "aar" - } - } - implementation libs.signal.ringrtc implementation libs.leolin.shortcutbadger diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt index 1fee2c1952..8bfbe575a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt @@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.lock.PinHashing import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity import org.thoughtcrime.securesms.lock.v2.KbsConstants import org.thoughtcrime.securesms.lock.v2.PinKeyboardType @@ -39,6 +38,7 @@ import org.thoughtcrime.securesms.util.ServiceUtil import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.whispersystems.signalservice.api.kbs.PinHashUtil class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFragment__account) { @@ -247,7 +247,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag pinEditText.typeface = Typeface.DEFAULT turnOffButton.setOnClickListener { val pin = pinEditText.text.toString() - val correct = PinHashing.verifyLocalPinHash(SignalStore.kbsValues().localPinHash!!, pin) + val correct = PinHashUtil.verifyLocalPinHash(SignalStore.kbsValues().localPinHash!!, pin) if (correct) { SignalStore.pinValues().setPinRemindersEnabled(false) viewModel.refreshState() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt index 257af43926..4e88a425b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt @@ -8,12 +8,12 @@ import androidx.navigation.fragment.findNavController import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.lock.PinHashing import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.SupportEmailUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.whispersystems.signalservice.api.kbs.PinHashUtil class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layout.fragment_change_number_registration_lock) { @@ -42,7 +42,7 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo } override fun handleSuccessfulPinEntry(pin: String) { - val pinsDiffer: Boolean = SignalStore.kbsValues().localPinHash?.let { !PinHashing.verifyLocalPinHash(it, pin) } ?: false + val pinsDiffer: Boolean = SignalStore.kbsValues().localPinHash?.let { !PinHashUtil.verifyLocalPinHash(it, pin) } ?: false pinButton.cancelSpinning() diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java index 712e4e0dc3..2bc5458725 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java @@ -4,10 +4,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.core.util.StringStringSerializer; -import org.thoughtcrime.securesms.lock.PinHashing; import org.thoughtcrime.securesms.util.JsonUtils; import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.signalservice.api.kbs.PinHashUtil; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; import java.io.IOException; @@ -75,7 +75,7 @@ public final class KbsValues extends SignalStoreValues { getStore().beginWrite() .putString(TOKEN_RESPONSE, tokenResponse) .putBlob(MASTER_KEY, masterKey.serialize()) - .putString(LOCK_LOCAL_PIN_HASH, PinHashing.localPinHash(pin)) + .putString(LOCK_LOCAL_PIN_HASH, PinHashUtil.localPinHash(pin)) .putString(PIN, pin) .putLong(LAST_CREATE_FAILED_TIMESTAMP, -1) .putBoolean(OPTED_OUT, false) diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/PinHashing.java b/app/src/main/java/org/thoughtcrime/securesms/lock/PinHashing.java deleted file mode 100644 index ff90e28e81..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/PinHashing.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.thoughtcrime.securesms.lock; - -import androidx.annotation.NonNull; - -import org.signal.argon2.Argon2; -import org.signal.argon2.Argon2Exception; -import org.signal.argon2.MemoryCost; -import org.signal.argon2.Type; -import org.signal.argon2.UnknownTypeException; -import org.signal.argon2.Version; -import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.signalservice.api.KeyBackupService; -import org.whispersystems.signalservice.api.kbs.HashedPin; -import org.whispersystems.signalservice.internal.registrationpin.PinHasher; - -public final class PinHashing { - - private PinHashing() { - } - - public static HashedPin hashPin(@NonNull String pin, @NonNull KeyBackupService.HashSession hashSession) { - return PinHasher.hashPin(PinHasher.normalize(pin), password -> { - try { - return new Argon2.Builder(Version.V13) - .type(Type.Argon2id) - .memoryCost(MemoryCost.MiB(16)) - .parallelism(1) - .iterations(32) - .hashLength(64) - .build() - .hash(password, hashSession.hashSalt()) - .getHash(); - } catch (Argon2Exception e) { - throw new AssertionError(e); - } - }); - } - - public static String localPinHash(@NonNull String pin) { - byte[] normalized = PinHasher.normalize(pin); - try { - return new Argon2.Builder(Version.V13) - .type(Type.Argon2i) - .memoryCost(MemoryCost.KiB(256)) - .parallelism(1) - .iterations(50) - .hashLength(32) - .build() - .hash(normalized, Util.getSecretBytes(16)) - .getEncoded(); - } catch (Argon2Exception e) { - throw new AssertionError(e); - } - } - - public static boolean verifyLocalPinHash(@NonNull String localPinHash, @NonNull String pin) { - byte[] normalized = PinHasher.normalize(pin); - try { - return Argon2.verify(localPinHash, normalized); - } catch (UnknownTypeException e) { - throw new AssertionError(e); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminderDialog.java b/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminderDialog.java index d46444eb6c..f43ffaf970 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminderDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/SignalPinReminderDialog.java @@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.lock.v2.KbsConstants; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ViewUtil; +import org.whispersystems.signalservice.api.kbs.PinHashUtil; import java.util.Objects; @@ -121,7 +122,7 @@ public final class SignalPinReminderDialog { if (text.length() >= KbsConstants.MINIMUM_PIN_LENGTH) { submit.setEnabled(true); - if (PinHashing.verifyLocalPinHash(localHash, text)) { + if (PinHashUtil.verifyLocalPinHash(localHash, text)) { dialog.dismiss(); mainCallback.onReminderCompleted(text, callback.hadWrongGuess()); } @@ -180,7 +181,7 @@ public final class SignalPinReminderDialog { if (pin.length() < KbsConstants.MINIMUM_PIN_LENGTH) return; - if (PinHashing.verifyLocalPinHash(localPinHash, pin)) { + if (PinHashUtil.verifyLocalPinHash(localPinHash, pin)) { callback.onPinCorrect(pin); } else { callback.onPinWrong(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/CreateKbsPinViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/CreateKbsPinViewModel.java index b9fdabe0af..7bd27f2741 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/CreateKbsPinViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/CreateKbsPinViewModel.java @@ -8,7 +8,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import org.thoughtcrime.securesms.util.SingleLiveEvent; -import org.whispersystems.signalservice.internal.registrationpin.PinValidityChecker; +import org.whispersystems.signalservice.api.kbs.PinValidityChecker; public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel { diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java index 378d821733..df12cdf404 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java @@ -8,15 +8,15 @@ import androidx.annotation.Nullable; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.svr2.PinHash; import org.thoughtcrime.securesms.KbsEnclave; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.lock.PinHashing; import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.KeyBackupService; import org.whispersystems.signalservice.api.KeyBackupServicePinException; import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; -import org.whispersystems.signalservice.api.kbs.HashedPin; +import org.whispersystems.signalservice.api.kbs.PinHashUtil; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; @@ -162,7 +162,7 @@ public class KbsRepository { try { Log.i(TAG, "Restoring pin from KBS"); - HashedPin hashedPin = PinHashing.hashPin(pin, session); + PinHash hashedPin = PinHashUtil.hashPin(pin, session.hashSalt()); KbsPinData kbsData = session.restorePin(hashedPin); if (kbsData != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java index 482cf5a6c0..3458d4d704 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java @@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.svr2.PinHash; import org.thoughtcrime.securesms.KbsEnclave; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.JobTracker; @@ -16,15 +17,14 @@ import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.StorageForcePushJob; import org.thoughtcrime.securesms.keyvalue.KbsValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.lock.PinHashing; import org.thoughtcrime.securesms.lock.RegistrationLockReminders; import org.thoughtcrime.securesms.lock.v2.PinKeyboardType; import org.thoughtcrime.securesms.megaphone.Megaphones; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.KeyBackupService; -import org.whispersystems.signalservice.api.kbs.HashedPin; import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.signalservice.api.kbs.PinHashUtil; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.IOException; @@ -119,8 +119,8 @@ public final class PinState { MasterKey masterKey = kbsValues.getOrCreateMasterKey(); KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(kbsEnclave); KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); - HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession); - KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey); + PinHash pinHash = PinHashUtil.hashPin(pin, pinChangeSession.hashSalt()); + KbsPinData kbsData = pinChangeSession.setPin(pinHash, masterKey); kbsValues.setKbsMasterKey(kbsData, pin); kbsValues.setPinForgottenOrSkipped(false); @@ -221,8 +221,8 @@ public final class PinState { MasterKey masterKey = kbsValues.getOrCreateMasterKey(); KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(kbsEnclave); KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); - HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession); - KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey); + PinHash pinHash = PinHashUtil.hashPin(pin, pinChangeSession.hashSalt()); + KbsPinData kbsData = pinChangeSession.setPin(pinHash, masterKey); pinChangeSession.enableRegistrationLock(masterKey); @@ -299,8 +299,8 @@ public final class PinState { KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave); KeyBackupService.PinChangeSession pinChangeSession = kbs.newPinChangeSession(); - HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession); - KbsPinData newData = pinChangeSession.setPin(hashedPin, masterKey); + PinHash pinHash = PinHashUtil.hashPin(pin, pinChangeSession.hashSalt()); + KbsPinData newData = pinChangeSession.setPin(pinHash, masterKey); SignalStore.kbsValues().setKbsMasterKey(newData, pin); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java index c2b5ff6d22..4b7a09d982 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java @@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.jobs.NewRegistrationUsernameSyncJob; import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob; import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.lock.PinHashing; import org.thoughtcrime.securesms.pin.KbsRepository; import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException; import org.thoughtcrime.securesms.pin.TokenData; @@ -32,6 +31,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; +import org.whispersystems.signalservice.api.kbs.PinHashUtil; import org.whispersystems.signalservice.api.push.exceptions.IncorrectCodeException; import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; @@ -281,7 +281,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel { String localPinHash = SignalStore.kbsValues().getLocalPinHash(); if (hasRecoveryPassword() && localPinHash != null) { - if (PinHashing.verifyLocalPinHash(localPinHash, pin)) { + if (PinHashUtil.verifyLocalPinHash(localPinHash, pin)) { Log.i(TAG, "Local pin matches input, attempting registration"); return ReRegistrationData.canProceed(new KbsPinData(SignalStore.kbsValues().getOrCreateMasterKey(), SignalStore.kbsValues().getRegistrationLockTokenResponse())); } else { diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java similarity index 67% rename from app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java rename to app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java index 05ad1179da..767183f99d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java @@ -2,29 +2,33 @@ package org.thoughtcrime.securesms.registration.v2; import org.junit.Test; import org.signal.core.util.StreamUtil; +import org.signal.libsignal.svr2.PinHash; import org.thoughtcrime.securesms.registration.v2.testdata.KbsTestVector; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; -import org.whispersystems.signalservice.api.kbs.HashedPin; import org.whispersystems.signalservice.api.kbs.KbsData; import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.signalservice.api.kbs.PinHashUtil; import org.whispersystems.signalservice.internal.util.JsonUtil; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.thoughtcrime.securesms.testutil.SecureRandomTestUtil.mockRandom; -public final class HashedPinKbsDataTest { +public final class PinHashKbsDataTest { @Test public void vectors_createNewKbsData() throws IOException { for (KbsTestVector vector : getKbsTestVectorList()) { - HashedPin hashedPin = HashedPin.fromArgon2Hash(vector.getArgon2Hash()); + PinHash pinHash = fromArgon2Hash(vector.getArgon2Hash()); - KbsData kbsData = hashedPin.createNewKbsData(MasterKey.createNew(mockRandom(vector.getMasterKey()))); + KbsData kbsData = PinHashUtil.createNewKbsData(pinHash, MasterKey.createNew(mockRandom(vector.getMasterKey()))); assertArrayEquals(vector.getMasterKey(), kbsData.getMasterKey().serialize()); assertArrayEquals(vector.getIvAndCipher(), kbsData.getCipherText()); @@ -36,9 +40,9 @@ public final class HashedPinKbsDataTest { @Test public void vectors_decryptKbsDataIVCipherText() throws IOException, InvalidCiphertextException { for (KbsTestVector vector : getKbsTestVectorList()) { - HashedPin hashedPin = HashedPin.fromArgon2Hash(vector.getArgon2Hash()); + PinHash hashedPin = fromArgon2Hash(vector.getArgon2Hash()); - KbsData kbsData = hashedPin.decryptKbsDataIVCipherText(vector.getIvAndCipher()); + KbsData kbsData = PinHashUtil.decryptKbsDataIVCipherText(hashedPin, vector.getIvAndCipher()); assertArrayEquals(vector.getMasterKey(), kbsData.getMasterKey().serialize()); assertArrayEquals(vector.getIvAndCipher(), kbsData.getCipherText()); @@ -57,4 +61,17 @@ public final class HashedPinKbsDataTest { return data; } } + + public static PinHash fromArgon2Hash(byte[] argon2Hash64) { + if (argon2Hash64.length != 64) throw new AssertionError(); + + byte[] K = Arrays.copyOfRange(argon2Hash64, 0, 32); + byte[] kbsAccessKey = Arrays.copyOfRange(argon2Hash64, 32, 64); + + PinHash mocked = mock(PinHash.class); + when(mocked.encryptionKey()).thenReturn(K); + when(mocked.accessKey()).thenReturn(kbsAccessKey); + + return mocked; + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHasher_normalize_Test.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashUtil_normalize_Test.java similarity index 87% rename from app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHasher_normalize_Test.java rename to app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashUtil_normalize_Test.java index 1a0c4bf2b3..e05605a08f 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHasher_normalize_Test.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashUtil_normalize_Test.java @@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.registration.v2; import org.junit.Test; import org.signal.core.util.StreamUtil; import org.thoughtcrime.securesms.registration.v2.testdata.PinSanitationVector; -import org.whispersystems.signalservice.internal.registrationpin.PinHasher; +import org.whispersystems.signalservice.api.kbs.PinHashUtil; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.JsonUtil; @@ -14,12 +14,12 @@ import java.util.Arrays; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -public final class PinHasher_normalize_Test { +public final class PinHashUtil_normalize_Test { @Test public void vectors_normalize() throws IOException { for (PinSanitationVector vector : getKbsPinSanitationTestVectorList()) { - byte[] normalized = PinHasher.normalize(vector.getPin()); + byte[] normalized = PinHashUtil.normalize(vector.getPin()); if (!Arrays.equals(vector.getBytes(), normalized)) { assertEquals(String.format("%s [%s]", vector.getName(), vector.getPin()), diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinValidityChecker_validity_Test.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinValidityChecker_validity_Test.java index 8d81eab775..380876fabf 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinValidityChecker_validity_Test.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinValidityChecker_validity_Test.java @@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.registration.v2; import org.junit.Test; import org.signal.core.util.StreamUtil; import org.thoughtcrime.securesms.registration.v2.testdata.PinValidityVector; -import org.whispersystems.signalservice.internal.registrationpin.PinValidityChecker; +import org.whispersystems.signalservice.api.kbs.PinValidityChecker; import org.whispersystems.signalservice.internal.util.JsonUtil; import java.io.IOException; diff --git a/dependencies.gradle b/dependencies.gradle index f6c883ddee..cf1fb263bf 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -106,7 +106,6 @@ dependencyResolutionManagement { alias('libsignal-client').to('org.signal', 'libsignal-client').versionRef('libsignal-client') alias('libsignal-android').to('org.signal', 'libsignal-android').versionRef('libsignal-client') alias('signal-aesgcmprovider').to('org.signal:aesgcmprovider:0.0.3') - alias('signal-argon2').to('org.signal:argon2:13.1') alias('signal-ringrtc').to('org.signal:ringrtc-android:2.26.3') alias('signal-android-database-sqlcipher').to('org.signal:sqlcipher-android:4.5.4-S1') diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java index f621b33630..1cda5ed349 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java @@ -2,10 +2,11 @@ package org.whispersystems.signalservice.api; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.logging.Log; +import org.signal.libsignal.svr2.PinHash; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; -import org.whispersystems.signalservice.api.kbs.HashedPin; import org.whispersystems.signalservice.api.kbs.KbsData; import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.signalservice.api.kbs.PinHashUtil; import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher; import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation; @@ -122,7 +123,7 @@ public class KeyBackupService { } @Override - public KbsPinData restorePin(HashedPin hashedPin) + public KbsPinData restorePin(PinHash hashedPin) throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, KeyBackupSystemNoDataException, InvalidKeyException { int attempt = 0; @@ -155,13 +156,13 @@ public class KeyBackupService { } } - private KbsPinData restorePin(HashedPin hashedPin, TokenResponse token) + private KbsPinData restorePin(PinHash hashedPin, TokenResponse token) throws UnauthenticatedResponseException, IOException, TokenException, KeyBackupSystemNoDataException, InvalidKeyException { try { final int remainingTries = token.getTries(); final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation(); - final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(hashedPin.getKbsAccessKey(), token, remoteAttestation, serviceId); + final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(hashedPin.accessKey(), token, remoteAttestation, serviceId); final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName); final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation); @@ -172,7 +173,7 @@ public class KeyBackupService { Log.i(TAG, "Restore " + status.getStatus()); switch (status.getStatus()) { case OK: - KbsData kbsData = hashedPin.decryptKbsDataIVCipherText(status.getData().toByteArray()); + KbsData kbsData = PinHashUtil.decryptKbsDataIVCipherText(hashedPin, status.getData().toByteArray()); MasterKey masterKey = kbsData.getMasterKey(); return new KbsPinData(masterKey, nextToken); case PIN_MISMATCH: @@ -206,8 +207,8 @@ public class KeyBackupService { } @Override - public KbsPinData setPin(HashedPin hashedPin, MasterKey masterKey) throws IOException, UnauthenticatedResponseException { - KbsData newKbsData = hashedPin.createNewKbsData(masterKey); + public KbsPinData setPin(PinHash pinHash, MasterKey masterKey) throws IOException, UnauthenticatedResponseException { + KbsData newKbsData = PinHashUtil.createNewKbsData(pinHash, masterKey); TokenResponse tokenResponse = putKbsData(newKbsData.getKbsAccessKey(), newKbsData.getCipherText(), enclaveName, @@ -274,13 +275,13 @@ public class KeyBackupService { public interface RestoreSession extends HashSession { - KbsPinData restorePin(HashedPin hashedPin) + KbsPinData restorePin(PinHash hashedPin) throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, KeyBackupSystemNoDataException, InvalidKeyException; } public interface PinChangeSession extends HashSession { /** Creates a PIN. Does nothing to registration lock. */ - KbsPinData setPin(HashedPin hashedPin, MasterKey masterKey) throws IOException, UnauthenticatedResponseException; + KbsPinData setPin(PinHash hashedPin, MasterKey masterKey) throws IOException, UnauthenticatedResponseException; /** Removes the PIN data from KBS. */ void removePin() throws IOException, UnauthenticatedResponseException; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java deleted file mode 100644 index 68509a01d1..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.whispersystems.signalservice.api.kbs; - -import org.whispersystems.signalservice.api.crypto.HmacSIV; -import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; - -import static java.util.Arrays.copyOfRange; - -/** - * Represents a hashed pin, from which you can create or decrypt KBS data. - *

- * Normal Java casing has been ignored to match original specifications. - */ -public final class HashedPin { - - private final byte[] K; - private final byte[] kbsAccessKey; - - private HashedPin(byte[] K, byte[] kbsAccessKey) { - this.K = K; - this.kbsAccessKey = kbsAccessKey; - } - - public static HashedPin fromArgon2Hash(byte[] argon2Hash64) { - if (argon2Hash64.length != 64) throw new AssertionError(); - - byte[] K = copyOfRange(argon2Hash64, 0, 32); - byte[] kbsAccessKey = copyOfRange(argon2Hash64, 32, 64); - return new HashedPin(K, kbsAccessKey); - } - - /** - * Creates a new {@link KbsData} to store on KBS. - */ - public KbsData createNewKbsData(MasterKey masterKey) { - byte[] M = masterKey.serialize(); - byte[] IVC = HmacSIV.encrypt(K, M); - return new KbsData(masterKey, kbsAccessKey, IVC); - } - - /** - * Takes 48 byte IVC from KBS and returns full {@link KbsData}. - */ - public KbsData decryptKbsDataIVCipherText(byte[] IVC) throws InvalidCiphertextException { - byte[] masterKey = HmacSIV.decrypt(K, IVC); - return new KbsData(new MasterKey(masterKey), kbsAccessKey, IVC); - } - - public byte[] getKbsAccessKey() { - return kbsAccessKey; - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java index 6a7abc6dc6..4350a38086 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java @@ -1,7 +1,7 @@ package org.whispersystems.signalservice.api.kbs; /** - * Construct from a {@link HashedPin}. + * Construct from a {@link org.signal.libsignal.svr2.PinHash}. */ public final class KbsData { private final MasterKey masterKey; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt new file mode 100644 index 0000000000..2b01c3a295 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.signalservice.api.kbs + +import org.signal.libsignal.svr2.Pin +import org.signal.libsignal.svr2.PinHash +import org.whispersystems.signalservice.api.crypto.HmacSIV +import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException +import java.nio.charset.StandardCharsets +import java.text.Normalizer + +object PinHashUtil { + + /** + * Takes a user-provided (i.e. non-normalized) PIN, normalizes it, and generates a [PinHash]. + */ + @JvmStatic + fun hashPin(pin: String, salt: ByteArray): PinHash { + return Pin.hash(normalize(pin), salt) + } + + /** + * Takes a user-provided (i.e. non-normalized) PIN, normalizes it, and generates a hash that is suitable for storing on-device + * for the purpose of checking PIN reminder correctness. Use in combination with [verifyLocalPinHash]. + */ + @JvmStatic + fun localPinHash(pin: String): String { + return Pin.localHash(normalize(pin)) + } + + /** + * Takes a user-provided (i.e. non-normalized) PIN, normalizes it, checks to see if it matches a hash that was generated with [localPinHash]. + */ + @JvmStatic + fun verifyLocalPinHash(localPinHash: String, pin: String): Boolean { + return Pin.verifyLocalHash(localPinHash, normalize(pin)) + } + + /** + * Creates a new [KbsData] to store on KBS. + */ + @JvmStatic + fun createNewKbsData(pinHash: PinHash, masterKey: MasterKey): KbsData { + val ivc = HmacSIV.encrypt(pinHash.encryptionKey(), masterKey.serialize()) + return KbsData(masterKey, pinHash.accessKey(), ivc) + } + + /** + * Takes 48 byte IVC from KBS and returns full [KbsData]. + */ + @JvmStatic + @Throws(InvalidCiphertextException::class) + fun decryptKbsDataIVCipherText(pinHash: PinHash, ivc: ByteArray?): KbsData { + val masterKey = HmacSIV.decrypt(pinHash.encryptionKey(), ivc) + return KbsData(MasterKey(masterKey), pinHash.accessKey(), ivc) + } + + /** + * Takes a user-input PIN string and normalizes it to a standard character set. + */ + @JvmStatic + fun normalize(pin: String): ByteArray { + var normalizedPin = pin.trim() + + if (PinString.allNumeric(normalizedPin)) { + normalizedPin = PinString.toArabic(normalizedPin) + } + + normalizedPin = Normalizer.normalize(normalizedPin, Normalizer.Form.NFKD) + + return normalizedPin.toByteArray(StandardCharsets.UTF_8) + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinString.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinString.java similarity index 81% rename from libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinString.java rename to libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinString.java index 7eef92e31e..f4afcfceb6 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinString.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinString.java @@ -1,4 +1,9 @@ -package org.whispersystems.signalservice.internal.registrationpin; +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.kbs; final class PinString { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinValidityChecker.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinValidityChecker.java similarity index 90% rename from libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinValidityChecker.java rename to libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinValidityChecker.java index eae9c51ff1..eb88aa7025 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinValidityChecker.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/PinValidityChecker.java @@ -1,4 +1,9 @@ -package org.whispersystems.signalservice.internal.registrationpin; +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.kbs; public final class PinValidityChecker { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinHasher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinHasher.java deleted file mode 100644 index e81c519bbc..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/registrationpin/PinHasher.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.whispersystems.signalservice.internal.registrationpin; - -import org.whispersystems.signalservice.api.kbs.HashedPin; - -import java.nio.charset.StandardCharsets; -import java.text.Normalizer; - -public final class PinHasher { - - public static byte[] normalize(String pin) { - pin = pin.trim(); - - if (PinString.allNumeric(pin)) { - pin = PinString.toArabic(pin); - } - - pin = Normalizer.normalize(pin, Normalizer.Form.NFKD); - - return pin.getBytes(StandardCharsets.UTF_8); - } - - public static HashedPin hashPin(byte[] normalizedPinBytes, Argon2 argon2) { - return HashedPin.fromArgon2Hash(argon2.hash(normalizedPinBytes)); - } - - public interface Argon2 { - byte[] hash(byte[] password); - } -}