Hmac-SIV encryption/decryption.

This commit is contained in:
Alan Evans
2020-01-17 13:31:30 -05:00
committed by Greyson Parrelli
parent 3907ec8b51
commit 7d70ea78cd
18 changed files with 462 additions and 28 deletions

View File

@@ -14,10 +14,10 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.storage.SignalStorageUtil;
import java.io.IOException;
@@ -60,8 +60,8 @@ public class MultiDeviceKeysUpdateJob extends BaseJob {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
byte[] masterKey = SignalStore.kbsValues().getMasterKey();
byte[] storageServiceKey = masterKey != null ? SignalStorageUtil.computeStorageServiceKey(masterKey)
MasterKey masterKey = SignalStore.kbsValues().getMasterKey();
byte[] storageServiceKey = masterKey != null ? masterKey.deriveStorageServiceKey()
: null;
if (storageServiceKey == null) {

View File

@@ -21,10 +21,10 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageUtil;
import java.io.IOException;
import java.util.ArrayList;
@@ -69,14 +69,14 @@ public class StorageForcePushJob extends BaseJob {
protected void onRun() throws IOException, RetryLaterException {
if (!FeatureFlags.STORAGE_SERVICE) throw new AssertionError();
byte[] kbsMasterKey = SignalStore.kbsValues().getMasterKey();
MasterKey kbsMasterKey = SignalStore.kbsValues().getMasterKey();
if (kbsMasterKey == null) {
Log.w(TAG, "No KBS master key is set! Must abort.");
return;
}
byte[] storageServiceKey = SignalStorageUtil.computeStorageServiceKey(kbsMasterKey);
byte[] storageServiceKey = kbsMasterKey.deriveStorageServiceKey();
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);

View File

@@ -29,11 +29,11 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageUtil;
import java.io.IOException;
import java.util.ArrayList;
@@ -110,14 +110,14 @@ public class StorageSyncJob extends BaseJob {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
byte[] kbsMasterKey = SignalStore.kbsValues().getMasterKey();
MasterKey kbsMasterKey = SignalStore.kbsValues().getMasterKey();
if (kbsMasterKey == null) {
Log.w(TAG, "No KBS master key is set! Must abort.");
return false;
}
byte[] storageServiceKey = SignalStorageUtil.computeStorageServiceKey(kbsMasterKey);
byte[] storageServiceKey = kbsMasterKey.deriveStorageServiceKey();
boolean needsMultiDeviceSync = false;
long localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
SignalStorageManifest remoteManifest = accountManager.getStorageManifest(storageServiceKey).or(new SignalStorageManifest(0, Collections.emptyList()));

View File

@@ -4,6 +4,7 @@ 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;
@@ -51,8 +52,13 @@ public final class KbsValues {
editor.commit();
}
public byte[] getMasterKey() {
return store.getBlob(REGISTRATION_LOCK_MASTER_KEY, null);
public @Nullable MasterKey getMasterKey() {
byte[] blob = store.getBlob(REGISTRATION_LOCK_MASTER_KEY, null);
if (blob != null) {
return new MasterKey(blob);
} else {
return null;
}
}
public @Nullable String getRegistrationLockToken() {

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.registration.v2;
import org.junit.Test;
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.internal.util.JsonUtil;
import java.io.IOException;
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.thoughtcrime.securesms.testutil.SecureRandomTestUtil.mockRandom;
public final class HashedPinKbsDataTest {
@Test
public void vectors_createNewKbsData() throws IOException {
for (KbsTestVector vector : getKbsTestVectorList().getVectors()) {
HashedPin hashedPin = HashedPin.fromArgon2Hash(vector.getArgon2Hash());
KbsData kbsData = hashedPin.createNewKbsData(mockRandom(vector.getMasterKey()));
assertArrayEquals(vector.getMasterKey(), kbsData.getMasterKey().serialize());
assertArrayEquals(vector.getIvAndCipher(), kbsData.getCipherText());
assertArrayEquals(vector.getKbsAccessKey(), kbsData.getKbsAccessKey());
assertEquals(vector.getRegistrationLock(), kbsData.getMasterKey().deriveRegistrationLock());
}
}
@Test
public void vectors_decryptKbsDataIVCipherText() throws IOException, InvalidCiphertextException {
for (KbsTestVector vector : getKbsTestVectorList().getVectors()) {
HashedPin hashedPin = HashedPin.fromArgon2Hash(vector.getArgon2Hash());
KbsData kbsData = hashedPin.decryptKbsDataIVCipherText(vector.getIvAndCipher());
assertArrayEquals(vector.getMasterKey(), kbsData.getMasterKey().serialize());
assertArrayEquals(vector.getIvAndCipher(), kbsData.getCipherText());
assertArrayEquals(vector.getKbsAccessKey(), kbsData.getKbsAccessKey());
assertEquals(vector.getRegistrationLock(), kbsData.getMasterKey().deriveRegistrationLock());
}
}
private static KbsTestVectorList getKbsTestVectorList() throws IOException {
try (InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream("data/kbs_vectors.json")) {
KbsTestVectorList data = JsonUtil.fromJson(Util.readFullyAsString(resourceAsStream), KbsTestVectorList.class);
assertFalse(data.getVectors().isEmpty());
return data;
}
}
}

View File

@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.registration.v2;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.thoughtcrime.securesms.testutil.HexDeserializer;
public final class KbsTestVector {
@JsonProperty("backup_id")
@JsonDeserialize(using = HexDeserializer.class)
private byte[] backupId;
@JsonProperty("argon2_hash")
@JsonDeserialize(using = HexDeserializer.class)
private byte[] argon2Hash;
@JsonProperty("pin")
private String pin;
@JsonProperty("registration_lock")
private String registrationLock;
@JsonProperty("master_key")
@JsonDeserialize(using = HexDeserializer.class)
private byte[] masterKey;
@JsonProperty("kbs_access_key")
@JsonDeserialize(using = HexDeserializer.class)
private byte[] kbsAccessKey;
@JsonProperty("iv_and_cipher")
@JsonDeserialize(using = HexDeserializer.class)
private byte[] ivAndCipher;
public byte[] getBackupId() {
return backupId;
}
public byte[] getArgon2Hash() {
return argon2Hash;
}
public String getPin() {
return pin;
}
public String getRegistrationLock() {
return registrationLock;
}
public byte[] getMasterKey() {
return masterKey;
}
public byte[] getKbsAccessKey() {
return kbsAccessKey;
}
public byte[] getIvAndCipher() {
return ivAndCipher;
}
}

View File

@@ -0,0 +1,15 @@
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;
}
}

View File

@@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.testutil;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.thoughtcrime.securesms.util.Hex;
import java.io.IOException;
/**
* Jackson deserializes Json Strings to byte[] using Base64 by default, this allows Base16.
*/
public final class HexDeserializer extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return Hex.fromStringCondensed(p.getText());
}
}

View File

@@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.testutil;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.security.SecureRandom;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
public final class SecureRandomTestUtil {
private SecureRandomTestUtil() {
}
/**
* Creates a {@link SecureRandom} that returns exactly the {@param returnValue} the first time
* its {@link SecureRandom#nextBytes(byte[])}} method is called.
* <p>
* Any attempt to call with the incorrect length, or a second time will fail.
*/
public static SecureRandom mockRandom(byte[] returnValue) {
SecureRandom mock = mock(SecureRandom.class);
ArgumentCaptor<byte[]> argument = ArgumentCaptor.forClass(byte[].class);
doAnswer(new Answer<Void>() {
private int count;
@Override
public Void answer(InvocationOnMock invocation) {
assertEquals("SecureRandom Mock: nextBytes only expected to be called once", 1, ++count);
byte[] output = argument.getValue();
assertEquals("SecureRandom Mock: nextBytes byte[] length requested does not match byte[] setup", returnValue.length, output.length);
System.arraycopy(returnValue, 0, output, 0, returnValue.length);
return null;
}
}).when(mock).nextBytes(argument.capture());
return mock;
}
}

View File

@@ -0,0 +1,13 @@
{
"vectors": [
{
"pin": "password",
"backup_id": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"argon2_hash": "65AADD2441A6C1979A2EA515DBB7092112703378D6BD83E8C4FF7771F6A7733F88A787415A2ECD79DA0D1016A82A27C5C695C9A19B88B0AA1D35683280AA9A67",
"master_key": "202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F",
"kbs_access_key": "88A787415A2ECD79DA0D1016A82A27C5C695C9A19B88B0AA1D35683280AA9A67",
"iv_and_cipher": "B18815B9B6C159CA9BB7E4F0486BD977AE84BF807F03157091DD04425C921D7D4CA7D5C4E27E31FD75DEF120135434D7",
"registration_lock": "2bf7988224ba35d3554966c65e8dc8c54974b034bdd44cabfd3f15fdb185e3c6"
}
]
}