Skip to content

Commit

Permalink
Replace pinstretcher with Argon2 and new PIN encryption.
Browse files Browse the repository at this point in the history
  • Loading branch information
alan-signal authored and greyson-signal committed Jan 24, 2020
1 parent f7a3bb2 commit e37c4b1
Show file tree
Hide file tree
Showing 32 changed files with 634 additions and 577 deletions.
22 changes: 8 additions & 14 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand All @@ -258,6 +250,8 @@ android {
universalApk true
}
}

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

compileOptions {
Expand Down Expand Up @@ -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 {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void onRun() throws IOException, UntrustedIdentityException {

SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();

MasterKey masterKey = SignalStore.kbsValues().getMasterKey();
MasterKey masterKey = SignalStore.kbsValues().getPinBackedMasterKey();
byte[] storageServiceKey = masterKey != null ? masterKey.deriveStorageServiceKey()
: null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private StorageForcePushJob(@NonNull Parameters parameters) {
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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ private boolean performSync() throws IOException, RetryLaterException, InvalidKe
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.");
Expand Down
111 changes: 69 additions & 42 deletions app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java
Original file line number Diff line number Diff line change
@@ -1,81 +1,108 @@
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;

KbsValues(KeyValueStore store) {
this.store = store;
}

public void setRegistrationLockMasterKey(@Nullable RegistrationLockData registrationLockData) {
KeyValueStore.Writer editor = store.beginWrite();
/**
* 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();
}

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());
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);
}

editor.commit();
store.beginWrite()
.putBoolean(V2_LOCK_ENABLED, true)
.putString(TOKEN_RESPONSE, tokenResponse)
.putBlob(MASTER_KEY, masterKey.serialize())
.putString(LOCK_LOCAL_PIN_HASH, localPinHash)
.commit();
}

public @Nullable MasterKey getMasterKey() {
byte[] blob = store.getBlob(REGISTRATION_LOCK_MASTER_KEY, null);
if (blob != null) {
return new MasterKey(blob);
} else {
return null;
/**
* 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;

Expand Down
63 changes: 63 additions & 0 deletions app/src/main/java/org/thoughtcrime/securesms/lock/PinHashing.java
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit e37c4b1

Please sign in to comment.