Replace pinstretcher with Argon2 and new PIN encryption.
parent
f7a3bb2ae8
commit
e37c4b1f87
|
@ -176,19 +176,11 @@ dependencies {
|
|||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.multidex:multidex:2.0.1'
|
||||
androidTestImplementation 'androidx.multidex:multidex-instrumentation:2.0.0'
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
|
||||
androidTestImplementation ('org.assertj:assertj-core:1.7.1') {
|
||||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||
}
|
||||
androidTestImplementation ('com.squareup.assertj:assertj-android:1.1.1') {
|
||||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
testImplementation 'org.robolectric:robolectric:4.2'
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
|
@ -238,8 +230,8 @@ android {
|
|||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||
buildConfigField "boolean", "DEV_BUILD", "false"
|
||||
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
|
||||
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"f2e2a5004794a6c1bac5c4949eadbc243dd02e02d1a93f10fe24584fb70815d8\""
|
||||
buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"f51f435802ada769e67aaf5744372bb7e7d519eecf996d335eb5b46b872b5789\""
|
||||
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"\""
|
||||
buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
|
@ -258,6 +250,8 @@ android {
|
|||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
@ -309,7 +303,7 @@ android {
|
|||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
|
||||
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"b5a865941f95887018c86725cc92308d34a3084dc2b4e7bd2de5e5e1690b50c6\""
|
||||
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"a1e9c1d3f352b5c4f0fc7a421b98119e60e5ff703c28fbea85c66bfa7306deab\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
}
|
||||
release {
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
public class TextSecureTestCase extends InstrumentationTestCase {
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
System.setProperty("dexmaker.dexcache", getInstrumentation().getTargetContext().getCacheDir().getPath());
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return getInstrumentation().getContext();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.thoughtcrime.securesms.lock;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.KbsData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class PinHashing_hashPin_Test {
|
||||
|
||||
@Test
|
||||
public void argon2_hashed_pin_password() throws IOException {
|
||||
byte[] backupId = Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin("password", () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
|
||||
assertArrayEquals(Hex.fromStringCondensed("ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8"), hashedPin.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("3f33ce58eb25b40436592a30eae2a8fabab1899095f4e2fba6e2d0dc43b4a2d9cac5a3931748522393951e0e54dec769"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argon2_hashed_pin_another_password() throws IOException {
|
||||
byte[] backupId = Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("88a787415a2ecd79da0d1016a82a27c5c695c9a19b88b0aa1d35683280aa9a67"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin("anotherpassword ", () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
|
||||
assertArrayEquals(Hex.fromStringCondensed("301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b"), hashedPin.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("9d9b05402ea39c17ff1c9298c8a0e86784a352aa02a74943bf8bcf07ec0f4b574a5b786ad0182c8d308d9eb06538b8c9"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
}
|
||||
}
|
|
@ -60,7 +60,7 @@ public class MultiDeviceKeysUpdateJob extends BaseJob {
|
|||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
|
||||
MasterKey masterKey = SignalStore.kbsValues().getMasterKey();
|
||||
MasterKey masterKey = SignalStore.kbsValues().getPinBackedMasterKey();
|
||||
byte[] storageServiceKey = masterKey != null ? masterKey.deriveStorageServiceKey()
|
||||
: null;
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ public class StorageForcePushJob extends BaseJob {
|
|||
protected void onRun() throws IOException, RetryLaterException {
|
||||
if (!FeatureFlags.STORAGE_SERVICE) throw new AssertionError();
|
||||
|
||||
MasterKey kbsMasterKey = SignalStore.kbsValues().getMasterKey();
|
||||
MasterKey kbsMasterKey = SignalStore.kbsValues().getPinBackedMasterKey();
|
||||
|
||||
if (kbsMasterKey == null) {
|
||||
Log.w(TAG, "No KBS master key is set! Must abort.");
|
||||
|
|
|
@ -110,7 +110,7 @@ public class StorageSyncJob extends BaseJob {
|
|||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||
MasterKey kbsMasterKey = SignalStore.kbsValues().getMasterKey();
|
||||
MasterKey kbsMasterKey = SignalStore.kbsValues().getPinBackedMasterKey();
|
||||
|
||||
if (kbsMasterKey == null) {
|
||||
Log.w(TAG, "No KBS master key is set! Must abort.");
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public final class KbsValues {
|
||||
|
||||
private static final String REGISTRATION_LOCK_PREF_V2 = "kbs.registration_lock_v2";
|
||||
private static final String REGISTRATION_LOCK_TOKEN_PREF = "kbs.registration_lock_token";
|
||||
private static final String REGISTRATION_LOCK_PIN_KEY_2_PREF = "kbs.registration_lock_pin_key_2";
|
||||
private static final String REGISTRATION_LOCK_MASTER_KEY = "kbs.registration_lock_master_key";
|
||||
private static final String REGISTRATION_LOCK_TOKEN_RESPONSE = "kbs.registration_lock_token_response";
|
||||
private static final String V2_LOCK_ENABLED = "kbs.v2_lock_enabled";
|
||||
private static final String MASTER_KEY = "kbs.registration_lock_master_key";
|
||||
private static final String TOKEN_RESPONSE = "kbs.token_response";
|
||||
private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash";
|
||||
|
||||
private final KeyValueStore store;
|
||||
|
||||
|
@ -24,58 +24,85 @@ public final class KbsValues {
|
|||
this.store = store;
|
||||
}
|
||||
|
||||
public void setRegistrationLockMasterKey(@Nullable RegistrationLockData registrationLockData) {
|
||||
KeyValueStore.Writer editor = store.beginWrite();
|
||||
|
||||
if (registrationLockData == null) {
|
||||
editor.remove(REGISTRATION_LOCK_PREF_V2)
|
||||
.remove(REGISTRATION_LOCK_TOKEN_RESPONSE)
|
||||
.remove(REGISTRATION_LOCK_MASTER_KEY)
|
||||
.remove(REGISTRATION_LOCK_TOKEN_PREF)
|
||||
.remove(REGISTRATION_LOCK_PIN_KEY_2_PREF);
|
||||
} else {
|
||||
PinStretcher.MasterKey masterKey = registrationLockData.getMasterKey();
|
||||
String tokenResponse;
|
||||
try {
|
||||
tokenResponse = JsonUtils.toJson(registrationLockData.getTokenResponse());
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
editor.putBoolean(REGISTRATION_LOCK_PREF_V2, true)
|
||||
.putString(REGISTRATION_LOCK_TOKEN_RESPONSE, tokenResponse)
|
||||
.putBlob(REGISTRATION_LOCK_MASTER_KEY, masterKey.getMasterKey())
|
||||
.putString(REGISTRATION_LOCK_TOKEN_PREF, masterKey.getRegistrationLock())
|
||||
.putBlob(REGISTRATION_LOCK_PIN_KEY_2_PREF, masterKey.getPinKey2());
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
/**
|
||||
* Deliberately does not clear the {@link #MASTER_KEY}.
|
||||
*/
|
||||
public void clearRegistrationLock() {
|
||||
store.beginWrite()
|
||||
.remove(V2_LOCK_ENABLED)
|
||||
.remove(TOKEN_RESPONSE)
|
||||
.remove(LOCK_LOCAL_PIN_HASH)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public @Nullable MasterKey getMasterKey() {
|
||||
byte[] blob = store.getBlob(REGISTRATION_LOCK_MASTER_KEY, null);
|
||||
if (blob != null) {
|
||||
return new MasterKey(blob);
|
||||
} else {
|
||||
return null;
|
||||
public synchronized void setRegistrationLockMasterKey(@NonNull RegistrationLockData registrationLockData, @NonNull String localPinHash) {
|
||||
MasterKey masterKey = registrationLockData.getMasterKey();
|
||||
String tokenResponse;
|
||||
try {
|
||||
tokenResponse = JsonUtils.toJson(registrationLockData.getTokenResponse());
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
store.beginWrite()
|
||||
.putBoolean(V2_LOCK_ENABLED, true)
|
||||
.putString(TOKEN_RESPONSE, tokenResponse)
|
||||
.putBlob(MASTER_KEY, masterKey.serialize())
|
||||
.putString(LOCK_LOCAL_PIN_HASH, localPinHash)
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds or creates the master key. Therefore this will always return a master key whether backed
|
||||
* up or not.
|
||||
* <p>
|
||||
* If you only want a key when it's backed up, use {@link #getPinBackedMasterKey()}.
|
||||
*/
|
||||
public synchronized @NonNull MasterKey getOrCreateMasterKey() {
|
||||
byte[] blob = store.getBlob(MASTER_KEY, null);
|
||||
|
||||
if (blob == null) {
|
||||
store.beginWrite()
|
||||
.putBlob(MASTER_KEY, MasterKey.createNew(new SecureRandom()).serialize())
|
||||
.commit();
|
||||
blob = store.getBlob(MASTER_KEY, null);
|
||||
}
|
||||
|
||||
return new MasterKey(blob);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if master key is not backed up by a pin.
|
||||
*/
|
||||
public synchronized @Nullable MasterKey getPinBackedMasterKey() {
|
||||
if (!isV2RegistrationLockEnabled()) return null;
|
||||
return getMasterKey();
|
||||
}
|
||||
|
||||
private synchronized @Nullable MasterKey getMasterKey() {
|
||||
byte[] blob = store.getBlob(MASTER_KEY, null);
|
||||
return blob != null ? new MasterKey(blob) : null;
|
||||
}
|
||||
|
||||
public @Nullable String getRegistrationLockToken() {
|
||||
return store.getString(REGISTRATION_LOCK_TOKEN_PREF, null);
|
||||
MasterKey masterKey = getPinBackedMasterKey();
|
||||
if (masterKey == null) {
|
||||
return null;
|
||||
} else {
|
||||
return masterKey.deriveRegistrationLock();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable byte[] getRegistrationLockPinKey2() {
|
||||
return store.getBlob(REGISTRATION_LOCK_PIN_KEY_2_PREF, null);
|
||||
public @Nullable String getLocalPinHash() {
|
||||
return store.getString(LOCK_LOCAL_PIN_HASH, null);
|
||||
}
|
||||
|
||||
public boolean isV2RegistrationLockEnabled() {
|
||||
return store.getBoolean(REGISTRATION_LOCK_PREF_V2, false);
|
||||
return store.getBoolean(V2_LOCK_ENABLED, false);
|
||||
}
|
||||
|
||||
public @Nullable
|
||||
TokenResponse getRegistrationLockTokenResponse() {
|
||||
String token = store.getString(REGISTRATION_LOCK_TOKEN_RESPONSE, null);
|
||||
public @Nullable TokenResponse getRegistrationLockTokenResponse() {
|
||||
String token = store.getString(TOKEN_RESPONSE, null);
|
||||
|
||||
if (token == null) return null;
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package org.thoughtcrime.securesms.lock;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.argon2.Argon2;
|
||||
import org.signal.argon2.Argon2Exception;
|
||||
import org.signal.argon2.MemoryCost;
|
||||
import org.signal.argon2.Type;
|
||||
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 static final Type KBS_PIN_ARGON_TYPE = Type.Argon2id;
|
||||
private static final Type LOCAL_PIN_ARGON_TYPE = Type.Argon2i;
|
||||
|
||||
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(KBS_PIN_ARGON_TYPE)
|
||||
.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(LOCAL_PIN_ARGON_TYPE)
|
||||
.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);
|
||||
return Argon2.verify(localPinHash, normalized, LOCAL_PIN_ARGON_TYPE);
|
||||
}
|
||||
}
|
|
@ -39,14 +39,15 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
|
|||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -54,12 +55,15 @@ public final class RegistrationLockDialog {
|
|||
|
||||
private static final String TAG = Log.tag(RegistrationLockDialog.class);
|
||||
|
||||
private static final int MIN_V2_NUMERIC_PIN_LENGTH_ENTRY = 4;
|
||||
private static final int MIN_V2_NUMERIC_PIN_LENGTH_SETTING = 4;
|
||||
|
||||
public static void showReminderIfNecessary(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
|
||||
if (!RegistrationLockReminders.needsReminder(context)) return;
|
||||
|
||||
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context) &&
|
||||
TextUtils.isEmpty(SignalStore.kbsValues().getRegistrationLockToken())) {
|
||||
!SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
|
||||
// Neither v1 or v2 to check against
|
||||
Log.w(TAG, "Reg lock enabled, but no pin stored to verify against");
|
||||
return;
|
||||
|
@ -117,62 +121,38 @@ public final class RegistrationLockDialog {
|
|||
//noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system.
|
||||
String pin = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
|
||||
|
||||
return new TextWatcher() {
|
||||
return new AfterTextChanged((Editable s) -> {
|
||||
if (s != null && s.toString().replace(" ", "").equals(pin)) {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, true);
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (s != null && s.toString().replace(" ", "").equals(pin)) {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, true);
|
||||
|
||||
if (FeatureFlags.KBS) {
|
||||
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
|
||||
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
|
||||
}
|
||||
if (FeatureFlags.KBS) {
|
||||
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
|
||||
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static TextWatcher getV2PinWatcher(@NonNull Context context, AlertDialog dialog) {
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
String registrationLockToken = kbsValues.getRegistrationLockToken();
|
||||
byte[] pinKey2 = kbsValues.getRegistrationLockPinKey2();
|
||||
TokenResponse registrationLockTokenResponse = kbsValues.getRegistrationLockTokenResponse();
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getPinBackedMasterKey();
|
||||
String localPinHash = kbsValues.getLocalPinHash();
|
||||
|
||||
if (registrationLockToken == null) throw new AssertionError("No V2 reg lock token set at time of reminder");
|
||||
if (pinKey2 == null) throw new AssertionError("No pin key2 set at time of reminder");
|
||||
if (registrationLockTokenResponse == null) throw new AssertionError("No registrationLockTokenResponse set at time of reminder");
|
||||
if (masterKey == null) throw new AssertionError("No masterKey set at time of reminder");
|
||||
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
|
||||
|
||||
return new TextWatcher() {
|
||||
return new AfterTextChanged((Editable s) -> {
|
||||
if (s == null) return;
|
||||
String pin = s.toString();
|
||||
if (TextUtils.isEmpty(pin)) return;
|
||||
if (pin.length() < MIN_V2_NUMERIC_PIN_LENGTH_ENTRY) return;
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (s == null) return;
|
||||
String pin = s.toString();
|
||||
if (TextUtils.isEmpty(pin)) return;
|
||||
if (pin.length() < 4) return;
|
||||
|
||||
try {
|
||||
if (registrationLockToken.equals(PinStretcher.stretchPin(pin).withPinKey2(pinKey2).getRegistrationLock())) {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, true);
|
||||
}
|
||||
} catch (InvalidPinException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, true);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
|
@ -198,8 +178,10 @@ public final class RegistrationLockDialog {
|
|||
String pinValue = pin.getText().toString().replace(" ", "");
|
||||
String repeatValue = repeat.getText().toString().replace(" ", "");
|
||||
|
||||
if (pinValue.length() < 4) {
|
||||
Toast.makeText(context, R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_four_digits, Toast.LENGTH_LONG).show();
|
||||
if (pinValue.length() < MIN_V2_NUMERIC_PIN_LENGTH_SETTING) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, MIN_V2_NUMERIC_PIN_LENGTH_SETTING),
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -226,27 +208,29 @@ public final class RegistrationLockDialog {
|
|||
TextSecurePreferences.setDeprecatedRegistrationLockPin(context, pinValue);
|
||||
} else {
|
||||
Log.i(TAG, "Setting pin on KBS");
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
RegistrationLockData kbsData = keyBackupService.newPinChangeSession()
|
||||
.setPin(pinValue);
|
||||
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
|
||||
.restorePin(pinValue);
|
||||
String restoredLock = restoredData.getMasterKey()
|
||||
.getRegistrationLock();
|
||||
|
||||
if (!restoredLock.equals(kbsData.getMasterKey().getRegistrationLock())) {
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
|
||||
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
|
||||
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
|
||||
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
|
||||
.restorePin(hashedPin);
|
||||
|
||||
if (!restoredData.getMasterKey().equals(masterKey)) {
|
||||
throw new AssertionError("Failed to set the pin correctly");
|
||||
} else {
|
||||
Log.i(TAG, "Set and retrieved pin on KBS successfully");
|
||||
}
|
||||
|
||||
SignalStore.kbsValues().setRegistrationLockMasterKey(restoredData);
|
||||
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
|
||||
TextSecurePreferences.clearOldRegistrationLockPin(context);
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
|
||||
}
|
||||
return true;
|
||||
} catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException | InvalidPinException e) {
|
||||
} catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
|
@ -308,7 +292,7 @@ public final class RegistrationLockDialog {
|
|||
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
keyBackupService.newPinChangeSession(currentToken).removePin();
|
||||
kbsValues.setRegistrationLockMasterKey(null);
|
||||
kbsValues.clearRegistrationLock();
|
||||
|
||||
// It is possible a migration has not occurred, in this case, we need to remove the old V1 Pin
|
||||
if (TextSecurePreferences.isV1RegistrationLockEnabled(context)) {
|
||||
|
|
|
@ -9,13 +9,18 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
|||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.jobs.BaseJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.KbsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.PinHashing;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -49,7 +54,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws IOException, UnauthenticatedResponseException {
|
||||
protected void onRun() throws IOException, UnauthenticatedResponseException, KeyBackupServicePinException {
|
||||
if (!FeatureFlags.KBS) {
|
||||
Log.i(TAG, "Not migrating pin to KBS");
|
||||
return;
|
||||
|
@ -61,27 +66,33 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
|
|||
}
|
||||
|
||||
//noinspection deprecation Only acceptable place to read the old pin.
|
||||
String registrationLockPin = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
|
||||
String pinValue = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
|
||||
|
||||
if (registrationLockPin == null | TextUtils.isEmpty(registrationLockPin)) {
|
||||
if (pinValue == null | TextUtils.isEmpty(pinValue)) {
|
||||
Log.i(TAG, "No old pin to migrate");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Migrating pin to Key Backup Service");
|
||||
|
||||
try {
|
||||
RegistrationLockData registrationPinV2Key = ApplicationDependencies.getKeyBackupService()
|
||||
.newPinChangeSession()
|
||||
.setPin(registrationLockPin);
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
|
||||
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
|
||||
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
|
||||
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
|
||||
.restorePin(hashedPin);
|
||||
|
||||
SignalStore.kbsValues().setRegistrationLockMasterKey(registrationPinV2Key);
|
||||
TextSecurePreferences.clearOldRegistrationLockPin(context);
|
||||
} catch (InvalidPinException e) {
|
||||
Log.w(TAG, "The V1 pin cannot be migrated.", e);
|
||||
return;
|
||||
if (!restoredData.getMasterKey().equals(masterKey)) {
|
||||
throw new RuntimeException("Failed to migrate the pin correctly");
|
||||
} else {
|
||||
Log.i(TAG, "Set and retrieved pin on KBS successfully");
|
||||
}
|
||||
|
||||
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
|
||||
TextSecurePreferences.clearOldRegistrationLockPin(context);
|
||||
|
||||
Log.i(TAG, "Pin migrated to Key Backup Service");
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,12 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
|
||||
disablePassphrase = (CheckBoxPreference) this.findPreference("pref_enable_passphrase_temporary");
|
||||
|
||||
this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1).setOnPreferenceClickListener(new AccountLockClickListener());
|
||||
SwitchPreferenceCompat regLock = (SwitchPreferenceCompat) this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1);
|
||||
regLock.setChecked(
|
||||
TextSecurePreferences.isV1RegistrationLockEnabled(requireContext()) || SignalStore.kbsValues().isV2RegistrationLockEnabled()
|
||||
);
|
||||
regLock.setOnPreferenceClickListener(new AccountLockClickListener());
|
||||
|
||||
this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener());
|
||||
this.findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setOnPreferenceClickListener(new ScreenLockTimeoutListener());
|
||||
|
||||
|
@ -149,10 +154,12 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
private class AccountLockClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (((SwitchPreferenceCompat)preference).isChecked()) {
|
||||
RegistrationLockDialog.showRegistrationUnlockPrompt(requireContext(), (SwitchPreferenceCompat)preference);
|
||||
Context context = requireContext();
|
||||
|
||||
if (TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
|
||||
RegistrationLockDialog.showRegistrationUnlockPrompt(context, (SwitchPreferenceCompat)preference);
|
||||
} else {
|
||||
RegistrationLockDialog.showRegistrationLockPrompt(requireContext(), (SwitchPreferenceCompat)preference);
|
||||
RegistrationLockDialog.showRegistrationLockPrompt(context, (SwitchPreferenceCompat)preference);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -17,7 +17,9 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
|
||||
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.logging.Log;
|
||||
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
|
||||
|
@ -36,11 +38,12 @@ import org.whispersystems.signalservice.api.KeyBackupService;
|
|||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
@ -160,11 +163,12 @@ public final class CodeVerificationRequest {
|
|||
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
|
||||
RegistrationLockData kbsData = restoreMasterKey(pin, basicStorageCredentials, kbsTokenResponse);
|
||||
String registrationLock = kbsData != null ? kbsData.getMasterKey().getRegistrationLock() : null;
|
||||
String registrationLock = kbsData != null ? kbsData.getMasterKey().deriveRegistrationLock() : null;
|
||||
boolean present = fcmToken != null;
|
||||
String pinForServer = basicStorageCredentials == null ? pin : null;
|
||||
|
||||
UUID uuid = accountManager.verifyAccountWithCode(code, null, registrationId, !present,
|
||||
pin, registrationLock,
|
||||
pinForServer, registrationLock,
|
||||
unidentifiedAccessKey, universalUnidentifiedAccess);
|
||||
|
||||
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
|
@ -203,8 +207,8 @@ public final class CodeVerificationRequest {
|
|||
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true);
|
||||
TextSecurePreferences.setUnauthorizedReceived(context, false);
|
||||
SignalStore.kbsValues().setRegistrationLockMasterKey(kbsData);
|
||||
if (kbsData == null) {
|
||||
SignalStore.kbsValues().clearRegistrationLock();
|
||||
//noinspection deprecation Only acceptable place to write the old pin.
|
||||
TextSecurePreferences.setDeprecatedRegistrationLockPin(context, pin);
|
||||
//noinspection deprecation Only acceptable place to write the old pin enabled state.
|
||||
|
@ -216,6 +220,7 @@ public final class CodeVerificationRequest {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
SignalStore.kbsValues().setRegistrationLockMasterKey(kbsData, PinHashing.localPinHash(pin));
|
||||
repostPinToResetTries(context, pin, kbsData);
|
||||
}
|
||||
if (pin != null) {
|
||||
|
@ -227,19 +232,23 @@ public final class CodeVerificationRequest {
|
|||
private static void repostPinToResetTries(@NonNull Context context, @Nullable String pin, @NonNull RegistrationLockData kbsData) {
|
||||
if (!FeatureFlags.KBS) return;
|
||||
|
||||
if (pin == null) return;
|
||||
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
|
||||
try {
|
||||
RegistrationLockData newData = keyBackupService.newPinChangeSession(kbsData.getTokenResponse())
|
||||
.setPin(pin);
|
||||
SignalStore.kbsValues().setRegistrationLockMasterKey(newData);
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
||||
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(kbsData.getTokenResponse());
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
|
||||
RegistrationLockData newData = pinChangeSession.setPin(hashedPin, masterKey);
|
||||
|
||||
kbsValues.setRegistrationLockMasterKey(newData, PinHashing.localPinHash(pin));
|
||||
TextSecurePreferences.clearOldRegistrationLockPin(context);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "May have failed to reset pin attempts!", e);
|
||||
} catch (UnauthenticatedResponseException e) {
|
||||
Log.w(TAG, "Failed to reset pin attempts", e);
|
||||
} catch (InvalidPinException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,7 +276,8 @@ public final class CodeVerificationRequest {
|
|||
|
||||
try {
|
||||
Log.i(TAG, "Restoring pin from KBS");
|
||||
RegistrationLockData kbsData = session.restorePin(pin);
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, session);
|
||||
RegistrationLockData kbsData = session.restorePin(hashedPin);
|
||||
if (kbsData != null) {
|
||||
Log.i(TAG, "Found registration lock token on KBS.");
|
||||
} else {
|
||||
|
@ -280,9 +290,6 @@ public final class CodeVerificationRequest {
|
|||
} catch (KeyBackupServicePinException e) {
|
||||
Log.w(TAG, "Incorrect pin", e);
|
||||
throw new KeyBackupSystemWrongPinException(e.getToken());
|
||||
} catch (InvalidPinException e) {
|
||||
Log.w(TAG, "Invalid pin", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -162,7 +162,7 @@ public class TextSecurePreferences {
|
|||
@Deprecated
|
||||
private static final String REGISTRATION_LOCK_PIN_PREF_V1 = "pref_registration_lock_pin";
|
||||
|
||||
private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME = "pref_registration_lock_last_reminder_time";
|
||||
private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME = "pref_registration_lock_last_reminder_time_2";
|
||||
private static final String REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL = "pref_registration_lock_next_reminder_interval";
|
||||
|
||||
private static final String SERVICE_OUTAGE = "pref_service_outage";
|
||||
|
|
|
@ -1737,7 +1737,7 @@
|
|||
<string name="RegistrationLockDialog_registration_lock_helps_protect_your_phone_number_from_unauthorized_registration_attempts">Registration Lock helps protect your phone number from unauthorized registration attempts. This feature can be disabled at any time in your Signal privacy settings</string>
|
||||
<string name="RegistrationLockDialog_registration_lock">Registration Lock</string>
|
||||
<string name="RegistrationLockDialog_enable">Enable</string>
|
||||
<string name="RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_four_digits">The Registration Lock PIN must be at least 4 digits.</string>
|
||||
<string name="RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits">The Registration Lock PIN must be at least %d digits.</string>
|
||||
<string name="RegistrationLockDialog_the_two_pins_you_entered_do_not_match">The two PINs you entered do not match.</string>
|
||||
<string name="RegistrationLockDialog_error_connecting_to_the_service">Error connecting to the service</string>
|
||||
<string name="RegistrationLockDialog_disable_registration_lock_pin">Disable Registration Lock PIN?</string>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package org.thoughtcrime.securesms.registration.v2;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.registration.v2.testdata.KbsTestVector;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
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.internal.util.JsonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -12,17 +14,17 @@ import java.io.InputStream;
|
|||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.testutil.SecureRandomTestUtil.mockRandom;
|
||||
|
||||
public final class HashedPinKbsDataTest {
|
||||
|
||||
@Test
|
||||
public void vectors_createNewKbsData() throws IOException {
|
||||
for (KbsTestVector vector : getKbsTestVectorList().getVectors()) {
|
||||
for (KbsTestVector vector : getKbsTestVectorList()) {
|
||||
HashedPin hashedPin = HashedPin.fromArgon2Hash(vector.getArgon2Hash());
|
||||
|
||||
KbsData kbsData = hashedPin.createNewKbsData(mockRandom(vector.getMasterKey()));
|
||||
KbsData kbsData = hashedPin.createNewKbsData(MasterKey.createNew(mockRandom(vector.getMasterKey())));
|
||||
|
||||
assertArrayEquals(vector.getMasterKey(), kbsData.getMasterKey().serialize());
|
||||
assertArrayEquals(vector.getIvAndCipher(), kbsData.getCipherText());
|
||||
|
@ -33,7 +35,7 @@ public final class HashedPinKbsDataTest {
|
|||
|
||||
@Test
|
||||
public void vectors_decryptKbsDataIVCipherText() throws IOException, InvalidCiphertextException {
|
||||
for (KbsTestVector vector : getKbsTestVectorList().getVectors()) {
|
||||
for (KbsTestVector vector : getKbsTestVectorList()) {
|
||||
HashedPin hashedPin = HashedPin.fromArgon2Hash(vector.getArgon2Hash());
|
||||
|
||||
KbsData kbsData = hashedPin.decryptKbsDataIVCipherText(vector.getIvAndCipher());
|
||||
|
@ -45,12 +47,12 @@ public final class HashedPinKbsDataTest {
|
|||
}
|
||||
}
|
||||
|
||||
private static KbsTestVectorList getKbsTestVectorList() throws IOException {
|
||||
private static KbsTestVector[] getKbsTestVectorList() throws IOException {
|
||||
try (InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream("data/kbs_vectors.json")) {
|
||||
|
||||
KbsTestVectorList data = JsonUtil.fromJson(Util.readFullyAsString(resourceAsStream), KbsTestVectorList.class);
|
||||
KbsTestVector[] data = JsonUtil.fromJson(Util.readFullyAsString(resourceAsStream), KbsTestVector[].class);
|
||||
|
||||
assertFalse(data.getVectors().isEmpty());
|
||||
assertTrue(data.length > 0);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
package org.thoughtcrime.securesms.registration.v2;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class KbsTestVectorList {
|
||||
|
||||
@JsonProperty("vectors")
|
||||
private List<KbsTestVector> vectors;
|
||||
|
||||
public List<KbsTestVector> getVectors() {
|
||||
return vectors;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package org.thoughtcrime.securesms.registration.v2;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.registration.v2.testdata.PinSanitationVector;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.PinHasher;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class PinHasher_normalize_Test {
|
||||
|
||||
@Test
|
||||
public void vectors_normalize() throws IOException {
|
||||
for (PinSanitationVector vector : getKbsPinSanitationTestVectorList()) {
|
||||
byte[] normalized = PinHasher.normalize(vector.getPin());
|
||||
|
||||
if (!Arrays.equals(vector.getBytes(), normalized)) {
|
||||
assertEquals(String.format("%s [%s]", vector.getName(), vector.getPin()),
|
||||
Hex.toStringCondensed(vector.getBytes()),
|
||||
Hex.toStringCondensed(normalized));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PinSanitationVector[] getKbsPinSanitationTestVectorList() throws IOException {
|
||||
try (InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream("data/kbs_pin_normalization_vectors.json")) {
|
||||
|
||||
PinSanitationVector[] data = JsonUtil.fromJson(Util.readFullyAsString(resourceAsStream), PinSanitationVector[].class);
|
||||
|
||||
assertTrue(data.length > 0);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.thoughtcrime.securesms.registration.v2;
|
||||
package org.thoughtcrime.securesms.registration.v2.testdata;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
|
@ -0,0 +1,31 @@
|
|||
package org.thoughtcrime.securesms.registration.v2.testdata;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
import org.thoughtcrime.securesms.testutil.HexDeserializer;
|
||||
|
||||
public class PinSanitationVector {
|
||||
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("pin")
|
||||
private String pin;
|
||||
|
||||
@JsonProperty("bytes")
|
||||
@JsonDeserialize(using = HexDeserializer.class)
|
||||
private byte[] bytes;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
[
|
||||
{
|
||||
"name": "Empty",
|
||||
"pin": "",
|
||||
"bytes": ""
|
||||
},
|
||||
{
|
||||
"pin": "password",
|
||||
"bytes": "70617373776f7264"
|
||||
},
|
||||
{
|
||||
"name": "Trailing space",
|
||||
"pin": "password ",
|
||||
"bytes": "70617373776f7264"
|
||||
},
|
||||
{
|
||||
"name": "Leading and trailing spaces",
|
||||
"pin": " password ",
|
||||
"bytes": "70617373776f7264"
|
||||
},
|
||||
{
|
||||
"name": "Space in word",
|
||||
"pin": "pass word",
|
||||
"bytes": "7061737320776f7264"
|
||||
},
|
||||
{
|
||||
"name": "Leading and trailing spaces and space in word",
|
||||
"pin": " pass word ",
|
||||
"bytes": "7061737320776f7264"
|
||||
},
|
||||
{
|
||||
"name": "Arabic digits",
|
||||
"pin": "12345",
|
||||
"bytes": "3132333435"
|
||||
},
|
||||
{
|
||||
"name": "Leading and trailing spaces around digits",
|
||||
"pin": " 12345 ",
|
||||
"bytes": "3132333435"
|
||||
},
|
||||
{
|
||||
"name": "Non-arabic digits",
|
||||
"pin": "١٢٣٤٥",
|
||||
"bytes": "3132333435"
|
||||
},
|
||||
{
|
||||
"name": "Mixed digits",
|
||||
"pin": "١٢٣4٥",
|
||||
"bytes": "3132333435"
|
||||
},
|
||||
{
|
||||
"name": "Non-arabic digits with non-digit",
|
||||
"pin": "١٢٣٤٥A",
|
||||
"bytes": "d9a1d9a2d9a3d9a4d9a541"
|
||||
},
|
||||
{
|
||||
"name": "NFKD Test, Double Char",
|
||||
"pin": "Ä",
|
||||
"bytes": "41cc88"
|
||||
},
|
||||
{
|
||||
"name": "NFKD Test, Single Char",
|
||||
"pin": "Ä",
|
||||
"bytes": "41cc88"
|
||||
}
|
||||
]
|
|
@ -1,13 +1,20 @@
|
|||
{
|
||||
"vectors": [
|
||||
{
|
||||
"pin": "password",
|
||||
"backup_id": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
|
||||
"argon2_hash": "65AADD2441A6C1979A2EA515DBB7092112703378D6BD83E8C4FF7771F6A7733F88A787415A2ECD79DA0D1016A82A27C5C695C9A19B88B0AA1D35683280AA9A67",
|
||||
"master_key": "202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F",
|
||||
"kbs_access_key": "88A787415A2ECD79DA0D1016A82A27C5C695C9A19B88B0AA1D35683280AA9A67",
|
||||
"iv_and_cipher": "B18815B9B6C159CA9BB7E4F0486BD977AE84BF807F03157091DD04425C921D7D4CA7D5C4E27E31FD75DEF120135434D7",
|
||||
"registration_lock": "2bf7988224ba35d3554966c65e8dc8c54974b034bdd44cabfd3f15fdb185e3c6"
|
||||
}
|
||||
]
|
||||
}
|
||||
[
|
||||
{
|
||||
"pin":"password",
|
||||
"backup_id":"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
||||
"argon2_hash":"44652df80490fc66bb864a9e638b2f7dc9e20649671dd66bbb9c37bee2bfecf1ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8",
|
||||
"master_key":"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
||||
"kbs_access_key":"ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8",
|
||||
"iv_and_cipher":"3f33ce58eb25b40436592a30eae2a8fabab1899095f4e2fba6e2d0dc43b4a2d9cac5a3931748522393951e0e54dec769",
|
||||
"registration_lock":"2bf7988224ba35d3554966c65e8dc8c54974b034bdd44cabfd3f15fdb185e3c6"
|
||||
},
|
||||
{
|
||||
"pin":"anotherpassword",
|
||||
"backup_id":"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
||||
"argon2_hash":"b6f16aa0591732e339b7e99cdd5fd6586a1c285c9d66876947fd82f66ed99757301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b",
|
||||
"master_key":"88a787415a2ecd79da0d1016a82a27c5c695c9a19b88b0aa1d35683280aa9a67",
|
||||
"kbs_access_key":"301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b",
|
||||
"iv_and_cipher":"9d9b05402ea39c17ff1c9298c8a0e86784a352aa02a74943bf8bcf07ec0f4b574a5b786ad0182c8d308d9eb06538b8c9",
|
||||
"registration_lock":"4a458afa1b07493b23ee9b3f287b70416b2388ca39b5b8c27b4b7585bf73f413"
|
||||
}
|
||||
]
|
|
@ -2,6 +2,9 @@ package org.whispersystems.signalservice.api;
|
|||
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
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.internal.contacts.crypto.KeyBackupCipher;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
|
||||
|
@ -14,8 +17,7 @@ import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse
|
|||
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.SignalStorage;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
|
@ -116,8 +118,13 @@ public final class KeyBackupService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public RegistrationLockData restorePin(String pin)
|
||||
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException
|
||||
public byte[] hashSalt() {
|
||||
return currentToken.getBackupId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistrationLockData restorePin(HashedPin hashedPin)
|
||||
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException
|
||||
{
|
||||
int attempt = 0;
|
||||
SecureRandom random = new SecureRandom();
|
||||
|
@ -128,7 +135,7 @@ public final class KeyBackupService {
|
|||
attempt++;
|
||||
|
||||
try {
|
||||
return restorePin(pin, token);
|
||||
return restorePin(hashedPin, token);
|
||||
} catch (TokenException tokenException) {
|
||||
|
||||
token = tokenException.getToken();
|
||||
|
@ -149,15 +156,13 @@ public final class KeyBackupService {
|
|||
}
|
||||
}
|
||||
|
||||
private RegistrationLockData restorePin(String pin, TokenResponse token)
|
||||
throws UnauthenticatedResponseException, IOException, TokenException, InvalidPinException
|
||||
private RegistrationLockData restorePin(HashedPin hashedPin, TokenResponse token)
|
||||
throws UnauthenticatedResponseException, IOException, TokenException
|
||||
{
|
||||
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
|
||||
|
||||
try {
|
||||
final int remainingTries = token.getTries();
|
||||
final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||
final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(stretchedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName));
|
||||
final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(hashedPin.getKbsAccessKey(), token, remoteAttestation, Hex.fromStringCondensed(enclaveName));
|
||||
final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||
final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation);
|
||||
|
||||
|
@ -169,7 +174,8 @@ public final class KeyBackupService {
|
|||
switch (status.getStatus()) {
|
||||
case OK:
|
||||
Log.i(TAG, String.format(Locale.US,"Restore OK! data: %s tries: %d", Hex.toStringCondensed(status.getData().toByteArray()), status.getTries()));
|
||||
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(status.getData().toByteArray());
|
||||
KbsData kbsData = hashedPin.decryptKbsDataIVCipherText(status.getData().toByteArray());
|
||||
MasterKey masterKey = kbsData.getMasterKey();
|
||||
return new RegistrationLockData(masterKey, nextToken);
|
||||
case PIN_MISMATCH:
|
||||
Log.i(TAG, "Restore PIN_MISMATCH");
|
||||
|
@ -203,16 +209,15 @@ public final class KeyBackupService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public RegistrationLockData setPin(String pin) throws IOException, UnauthenticatedResponseException, InvalidPinException {
|
||||
PinStretcher.MasterKey masterKey = PinStretcher.stretchPin(pin)
|
||||
.withNewSecurePinKey2();
|
||||
public RegistrationLockData setPin(HashedPin hashedPin, MasterKey masterKey) throws IOException, UnauthenticatedResponseException {
|
||||
KbsData newKbsData = hashedPin.createNewKbsData(masterKey);
|
||||
|
||||
TokenResponse tokenResponse = putKbsData(masterKey.getKbsAccessKey(),
|
||||
masterKey.getPinKey2(),
|
||||
TokenResponse tokenResponse = putKbsData(newKbsData.getKbsAccessKey(),
|
||||
newKbsData.getCipherText(),
|
||||
enclaveName,
|
||||
currentToken);
|
||||
|
||||
pushServiceSocket.setRegistrationLock(masterKey.getRegistrationLock());
|
||||
pushServiceSocket.setRegistrationLock(masterKey.deriveRegistrationLock());
|
||||
|
||||
return new RegistrationLockData(masterKey, tokenResponse);
|
||||
}
|
||||
|
@ -264,16 +269,21 @@ public final class KeyBackupService {
|
|||
}
|
||||
}
|
||||
|
||||
public interface RestoreSession {
|
||||
public interface HashSession {
|
||||
|
||||
RegistrationLockData restorePin(String pin)
|
||||
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, InvalidPinException;
|
||||
byte[] hashSalt();
|
||||
}
|
||||
|
||||
public interface PinChangeSession {
|
||||
public interface RestoreSession extends HashSession {
|
||||
|
||||
RegistrationLockData setPin(String pin)
|
||||
throws IOException, UnauthenticatedResponseException, InvalidPinException;
|
||||
RegistrationLockData restorePin(HashedPin hashedPin)
|
||||
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException;
|
||||
}
|
||||
|
||||
public interface PinChangeSession extends HashSession {
|
||||
|
||||
RegistrationLockData setPin(HashedPin hashedPin, MasterKey masterKey)
|
||||
throws IOException, UnauthenticatedResponseException;
|
||||
|
||||
void removePin()
|
||||
throws IOException, UnauthenticatedResponseException;
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
||||
|
||||
public final class RegistrationLockData {
|
||||
|
||||
private final PinStretcher.MasterKey masterKey;
|
||||
private final TokenResponse tokenResponse;
|
||||
private final MasterKey masterKey;
|
||||
private final TokenResponse tokenResponse;
|
||||
|
||||
RegistrationLockData(PinStretcher.MasterKey masterKey, TokenResponse tokenResponse) {
|
||||
RegistrationLockData(MasterKey masterKey, TokenResponse tokenResponse) {
|
||||
this.masterKey = masterKey;
|
||||
this.tokenResponse = tokenResponse;
|
||||
}
|
||||
|
||||
public PinStretcher.MasterKey getMasterKey() {
|
||||
public MasterKey getMasterKey() {
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ package org.whispersystems.signalservice.api.kbs;
|
|||
import org.whispersystems.signalservice.api.crypto.HmacSIV;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static java.util.Arrays.copyOfRange;
|
||||
|
||||
/**
|
||||
|
@ -33,11 +31,10 @@ public final class HashedPin {
|
|||
/**
|
||||
* Creates a new {@link KbsData} to store on KBS.
|
||||
*/
|
||||
public KbsData createNewKbsData(SecureRandom random) {
|
||||
byte[] M = new byte[32];
|
||||
random.nextBytes(M);
|
||||
public KbsData createNewKbsData(MasterKey masterKey) {
|
||||
byte[] M = masterKey.serialize();
|
||||
byte[] IVC = HmacSIV.encrypt(K, M);
|
||||
return new KbsData(M, kbsAccessKey, IVC);
|
||||
return new KbsData(masterKey, kbsAccessKey, IVC);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,6 +42,10 @@ public final class HashedPin {
|
|||
*/
|
||||
public KbsData decryptKbsDataIVCipherText(byte[] IVC) throws InvalidCiphertextException {
|
||||
byte[] masterKey = HmacSIV.decrypt(K, IVC);
|
||||
return new KbsData(masterKey, kbsAccessKey, IVC);
|
||||
return new KbsData(new MasterKey(masterKey), kbsAccessKey, IVC);
|
||||
}
|
||||
|
||||
public byte[] getKbsAccessKey() {
|
||||
return kbsAccessKey;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ public final class KbsData {
|
|||
private final byte[] kbsAccessKey;
|
||||
private final byte[] cipherText;
|
||||
|
||||
KbsData(byte[] masterKey, byte[] kbsAccessKey, byte[] cipherText) {
|
||||
this.masterKey = new MasterKey(masterKey);
|
||||
KbsData(MasterKey masterKey, byte[] kbsAccessKey, byte[] cipherText) {
|
||||
this.masterKey = masterKey;
|
||||
this.kbsAccessKey = kbsAccessKey;
|
||||
this.cipherText = cipherText;
|
||||
}
|
||||
|
|
|
@ -3,18 +3,29 @@ package org.whispersystems.signalservice.api.kbs;
|
|||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.util.StringUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.whispersystems.signalservice.api.crypto.CryptoUtil.hmacSha256;
|
||||
|
||||
public final class MasterKey {
|
||||
|
||||
private static final int LENGTH = 32;
|
||||
|
||||
private final byte[] masterKey;
|
||||
|
||||
public MasterKey(byte[] masterKey) {
|
||||
if (masterKey.length != 32) throw new AssertionError();
|
||||
if (masterKey.length != LENGTH) throw new AssertionError();
|
||||
|
||||
this.masterKey = masterKey;
|
||||
}
|
||||
|
||||
public static MasterKey createNew(SecureRandom secureRandom) {
|
||||
byte[] key = new byte[LENGTH];
|
||||
secureRandom.nextBytes(key);
|
||||
return new MasterKey(key);
|
||||
}
|
||||
|
||||
public String deriveRegistrationLock() {
|
||||
return Hex.toStringCondensed(derive("Registration Lock"));
|
||||
}
|
||||
|
@ -30,4 +41,16 @@ public final class MasterKey {
|
|||
public byte[] serialize() {
|
||||
return masterKey.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || o.getClass() != getClass()) return false;
|
||||
|
||||
return Arrays.equals(((MasterKey) o).masterKey, masterKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(masterKey);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
package org.whispersystems.signalservice.internal.registrationpin;
|
||||
|
||||
public final class InvalidPinException extends Exception {
|
||||
|
||||
InvalidPinException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
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 (allNumeric(pin)) {
|
||||
pin = new String(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);
|
||||
}
|
||||
|
||||
private static boolean allNumeric(CharSequence pin) {
|
||||
for (int i = 0; i < pin.length(); i++) {
|
||||
if (!Character.isDigit(pin.charAt(i))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string of not necessarily Arabic numerals to Arabic 0..9 characters.
|
||||
*/
|
||||
private static char[] toArabic(CharSequence numerals) {
|
||||
int length = numerals.length();
|
||||
char[] arabic = new char[length];
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int digit = Character.digit(numerals.charAt(i), 10);
|
||||
|
||||
arabic[i] = (char) ('0' + digit);
|
||||
}
|
||||
|
||||
return arabic;
|
||||
}
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
package org.whispersystems.signalservice.internal.registrationpin;
|
||||
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public final class PinStretcher {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
public static StretchedPin stretchPin(CharSequence pin) throws InvalidPinException {
|
||||
return new StretchedPin(pin);
|
||||
}
|
||||
|
||||
public static class StretchedPin {
|
||||
private final byte[] stretchedPin;
|
||||
private final byte[] pinKey1;
|
||||
private final byte[] kbsAccessKey;
|
||||
|
||||
private StretchedPin(byte[] stretchedPin, byte[] pinKey1, byte[] kbsAccessKey) {
|
||||
this.stretchedPin = stretchedPin;
|
||||
this.pinKey1 = pinKey1;
|
||||
this.kbsAccessKey = kbsAccessKey;
|
||||
}
|
||||
|
||||
private StretchedPin(CharSequence pin) throws InvalidPinException {
|
||||
if (pin.length() < 4) throw new InvalidPinException("Pin too short");
|
||||
|
||||
char[] arabicPin = toArabic(pin);
|
||||
|
||||
stretchedPin = pbkdf2HmacSHA256(arabicPin, "nosalt", 20000, 256);
|
||||
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA256);
|
||||
mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256));
|
||||
mac.update("Master Key Encryption".getBytes(UTF_8));
|
||||
|
||||
pinKey1 = new byte[32];
|
||||
mac.doFinal(pinKey1, 0);
|
||||
|
||||
mac.init(new SecretKeySpec(stretchedPin, HMAC_SHA256));
|
||||
mac.update("KBS Access Key".getBytes(UTF_8));
|
||||
|
||||
kbsAccessKey = new byte[32];
|
||||
mac.doFinal(kbsAccessKey, 0);
|
||||
} catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public MasterKey withPinKey2(byte[] pinKey2) {
|
||||
return new MasterKey(pinKey1, pinKey2, this);
|
||||
}
|
||||
|
||||
public MasterKey withNewSecurePinKey2() {
|
||||
return withPinKey2(Util.getSecretBytes(32));
|
||||
}
|
||||
|
||||
public byte[] getPinKey1() {
|
||||
return pinKey1;
|
||||
}
|
||||
|
||||
public byte[] getStretchedPin() {
|
||||
return stretchedPin;
|
||||
}
|
||||
|
||||
public byte[] getKbsAccessKey() {
|
||||
return kbsAccessKey;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MasterKey extends StretchedPin {
|
||||
private final byte[] pinKey2;
|
||||
private final byte[] masterKey;
|
||||
private final String registrationLock;
|
||||
|
||||
private MasterKey(byte[] pinKey1, byte[] pinKey2, StretchedPin stretchedPin) {
|
||||
super(stretchedPin.stretchedPin, stretchedPin.pinKey1, stretchedPin.kbsAccessKey);
|
||||
|
||||
if (pinKey2.length != 32) {
|
||||
throw new AssertionError("PinKey2 must be exactly 32 bytes");
|
||||
}
|
||||
|
||||
this.pinKey2 = pinKey2.clone();
|
||||
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA256);
|
||||
|
||||
mac.init(new SecretKeySpec(pinKey1, HMAC_SHA256));
|
||||
mac.update(pinKey2);
|
||||
|
||||
masterKey = new byte[32];
|
||||
mac.doFinal(masterKey, 0);
|
||||
|
||||
mac.init(new SecretKeySpec(masterKey, HMAC_SHA256));
|
||||
mac.update("Registration Lock".getBytes(UTF_8));
|
||||
|
||||
byte[] registration_lock_token_bytes = new byte[32];
|
||||
mac.doFinal(registration_lock_token_bytes, 0);
|
||||
registrationLock = Hex.toStringCondensed(registration_lock_token_bytes);
|
||||
|
||||
} catch (NoSuchAlgorithmException | ShortBufferException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getPinKey2() {
|
||||
return pinKey2;
|
||||
}
|
||||
|
||||
public String getRegistrationLock() {
|
||||
return registrationLock;
|
||||
}
|
||||
|
||||
public byte[] getMasterKey() {
|
||||
return masterKey;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] pbkdf2HmacSHA256(char[] pin, String salt, int iterationCount, int outputSize) {
|
||||
byte[] saltBytes = salt.getBytes(Charset.forName("UTF-8"));
|
||||
|
||||
try {
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
PBEKeySpec spec = new PBEKeySpec(pin, saltBytes, iterationCount, outputSize);
|
||||
byte[] encoded = skf.generateSecret(spec).getEncoded();
|
||||
|
||||
spec.clearPassword();
|
||||
|
||||
return encoded;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new AssertionError("Could not stretch pin", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string of not necessarily Arabic numerals to Arabic 0..9 characters.
|
||||
*/
|
||||
private static char[] toArabic(CharSequence numerals) throws InvalidPinException {
|
||||
int length = numerals.length();
|
||||
char[] arabic = new char[length];
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int digit = Character.digit(numerals.charAt(i), 10);
|
||||
|
||||
if (digit < 0) {
|
||||
throw new InvalidPinException("Pin must only consist of decimals");
|
||||
}
|
||||
|
||||
arabic[i] = (char) ('0' + digit);
|
||||
}
|
||||
|
||||
return arabic;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package org.whispersystems.signalservice.api.kbs;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public final class MasterKeyTest {
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void wrong_length_too_short() {
|
||||
new MasterKey(new byte[31]);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void wrong_length_too_long() {
|
||||
new MasterKey(new byte[33]);
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void invalid_input_null() {
|
||||
//noinspection ConstantConditions
|
||||
new MasterKey(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equality() {
|
||||
byte[] masterKeyBytes1 = new byte[32];
|
||||
byte[] masterKeyBytes2 = new byte[32];
|
||||
MasterKey masterKey1 = new MasterKey(masterKeyBytes1);
|
||||
MasterKey masterKey2 = new MasterKey(masterKeyBytes2);
|
||||
|
||||
assertEquals(masterKey1, masterKey2);
|
||||
assertEquals(masterKey1.hashCode(), masterKey2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void in_equality() {
|
||||
byte[] masterKeyBytes1 = new byte[32];
|
||||
byte[] masterKeyBytes2 = new byte[32];
|
||||
|
||||
masterKeyBytes1[0] = 1;
|
||||
|
||||
MasterKey masterKey1 = new MasterKey(masterKeyBytes1);
|
||||
MasterKey masterKey2 = new MasterKey(masterKeyBytes2);
|
||||
|
||||
assertNotEquals(masterKey1, masterKey2);
|
||||
assertNotEquals(masterKey1.hashCode(), masterKey2.hashCode());
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package org.whispersystems.signalservice.internal.registrationpin;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class PinStretchFailureTest {
|
||||
|
||||
@Test(expected = InvalidPinException.class)
|
||||
public void non_numeric_pin() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("A");
|
||||
}
|
||||
|
||||
@Test(expected = InvalidPinException.class)
|
||||
public void empty() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("");
|
||||
}
|
||||
|
||||
@Test(expected = InvalidPinException.class)
|
||||
public void too_few_digits() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("123");
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void pin_key_2_too_short() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("0000").withPinKey2(new byte[31]);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void pin_key_2_too_long() throws InvalidPinException {
|
||||
PinStretcher.stretchPin("0000").withPinKey2(new byte[33]);
|
||||
}
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
package org.whispersystems.signalservice.internal.registrationpin;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public final class PinStretchTest {
|
||||
|
||||
private final String pin;
|
||||
private final byte[] expectedStretchedPin;
|
||||
private final byte[] expectedKeyPin1;
|
||||
private final byte[] pinKey2;
|
||||
private final byte[] expectedMasterKey;
|
||||
private final String expectedRegistrationLock;
|
||||
private final byte[] expectedKbsAccessKey;
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[]{
|
||||
"12345",
|
||||
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
|
||||
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
|
||||
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
|
||||
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
|
||||
},
|
||||
new Object[]{
|
||||
"12345",
|
||||
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
|
||||
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"01198dc427cbf9c6b47f344654d75a263e53b992db73be44b201f357d072dc38",
|
||||
"bd1f4e129cc705c26c2fcebd3fbc6e7db60caade89e6c465c68ed60aeedbb0c3",
|
||||
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
|
||||
},
|
||||
new Object[]{
|
||||
"١٢٣٤٥",
|
||||
"4e84b9b2567e1999f665a4288fbc98a30fd7c4a6a1b504b07e56d4183107ff1d",
|
||||
"0191747f14295c6c2d42af3ff94d610b7899d5eb6cccd14c71aa314f70aaaf0f",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"892f2cab29c09b13718e5f06a3e4aa0dd42cd7e0b20c411668eed10bb06f72b2",
|
||||
"65cdbc33682f3be3c8809f54ed41c8f2f85cfce23b77d2a8b435ccff9681071d",
|
||||
"7a2d4f7974c4c2314bee8e68d62a03fd97af0ef6904ee1b912dcc900c19215ba"
|
||||
},
|
||||
new Object[]{
|
||||
"9876543210",
|
||||
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
|
||||
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"127a435c15be2528f4b735423f8ee558b789e8ea1f6fe64d144d5b21a87c4e06",
|
||||
"348d327acb823b54a988cf6bea647a154e21da25cbb121a115c13b871dccd548",
|
||||
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
|
||||
},
|
||||
new Object[]{
|
||||
"9876543210",
|
||||
"1ec376ca694b5c1fb185be3864343aaa08829833153f3a72813e3e48cb3579b9",
|
||||
"40f35cdc3f3325b037f9fedddd25c68b7ea9c3e50e6a1a81319c43263da7bec3",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"128833dbde1af3da703852b6b5a845e226fe9c7e069427b9c1e41279c0cdfb3a",
|
||||
"6be0b17899cfb5c4316b92acc7db3b6a2fa5b9a19ef3e58a1c84a4de49230aa6",
|
||||
"90aaa3156952db441a8c875e8e4abab3d48965df7f563fbfb39f567d1ec7354e",
|
||||
},
|
||||
new Object[]{
|
||||
"0123",
|
||||
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
|
||||
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
|
||||
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
|
||||
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
|
||||
},
|
||||
new Object[]{
|
||||
"௦௧௨௩",
|
||||
"b9bc227d893edc7cade32d16ba210599f9e901c721bcad85ad458ab90432cbe7",
|
||||
"bb8c8fc51b705dcdce43467ad7417fa5f28708941bcc9682fc4123a006701567",
|
||||
"abababababababababababababababababababababababababababababababab",
|
||||
"ca94b0a7b26d44078ccfcb88fd67151d891b3b8eb8c65ab94d536c3cb0e1d7dd",
|
||||
"d182bde40ee91969192d5166fc871cd4bf5e261b090bbc707354bddb29fb8290",
|
||||
"c0ae6e108296e507ee9ebd7fd5d8564b8e644bd53d50a2fc7ab379aea8074a91"
|
||||
});
|
||||
}
|
||||
|
||||
public PinStretchTest(String pin,
|
||||
String expectedStretchedPin,
|
||||
String expectedKeyPin1,
|
||||
String pinKey2,
|
||||
String expectedMasterKey,
|
||||
String expectedRegistrationLock,
|
||||
String expectedKbsAccessKey) throws IOException {
|
||||
this.pin = pin;
|
||||
this.expectedStretchedPin = Hex.fromStringCondensed(expectedStretchedPin);
|
||||
this.expectedKeyPin1 = Hex.fromStringCondensed(expectedKeyPin1);
|
||||
this.pinKey2 = Hex.fromStringCondensed(pinKey2);
|
||||
this.expectedMasterKey = Hex.fromStringCondensed(expectedMasterKey);
|
||||
this.expectedRegistrationLock = expectedRegistrationLock;
|
||||
this.expectedKbsAccessKey = Hex.fromStringCondensed(expectedKbsAccessKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretch_pin() throws InvalidPinException {
|
||||
PinStretcher.StretchedPin stretchedPin = PinStretcher.stretchPin(pin);
|
||||
|
||||
assertArrayEquals(expectedStretchedPin, stretchedPin.getStretchedPin());
|
||||
assertArrayEquals(expectedKeyPin1, stretchedPin.getPinKey1());
|
||||
assertArrayEquals(expectedKbsAccessKey, stretchedPin.getKbsAccessKey());
|
||||
|
||||
PinStretcher.MasterKey masterKey = stretchedPin.withPinKey2(pinKey2);
|
||||
|
||||
assertArrayEquals(pinKey2, masterKey.getPinKey2());
|
||||
assertArrayEquals(expectedMasterKey, masterKey.getMasterKey());
|
||||
assertEquals(expectedRegistrationLock, masterKey.getRegistrationLock());
|
||||
|
||||
assertArrayEquals(expectedStretchedPin, masterKey.getStretchedPin());
|
||||
assertArrayEquals(expectedKeyPin1, masterKey.getPinKey1());
|
||||
assertArrayEquals(expectedKbsAccessKey, masterKey.getKbsAccessKey());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue