mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 11:18:35 +00:00
Hmac-SIV encryption/decryption.
This commit is contained in:
parent
3907ec8b51
commit
7d70ea78cd
@ -14,10 +14,10 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
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.KeysMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@ -60,8 +60,8 @@ public class MultiDeviceKeysUpdateJob extends BaseJob {
|
|||||||
|
|
||||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||||
|
|
||||||
byte[] masterKey = SignalStore.kbsValues().getMasterKey();
|
MasterKey masterKey = SignalStore.kbsValues().getMasterKey();
|
||||||
byte[] storageServiceKey = masterKey != null ? SignalStorageUtil.computeStorageServiceKey(masterKey)
|
byte[] storageServiceKey = masterKey != null ? masterKey.deriveStorageServiceKey()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (storageServiceKey == null) {
|
if (storageServiceKey == null) {
|
||||||
|
@ -21,10 +21,10 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.libsignal.InvalidKeyException;
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
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.push.exceptions.PushNetworkException;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -69,14 +69,14 @@ public class StorageForcePushJob extends BaseJob {
|
|||||||
protected void onRun() throws IOException, RetryLaterException {
|
protected void onRun() throws IOException, RetryLaterException {
|
||||||
if (!FeatureFlags.STORAGE_SERVICE) throw new AssertionError();
|
if (!FeatureFlags.STORAGE_SERVICE) throw new AssertionError();
|
||||||
|
|
||||||
byte[] kbsMasterKey = SignalStore.kbsValues().getMasterKey();
|
MasterKey kbsMasterKey = SignalStore.kbsValues().getMasterKey();
|
||||||
|
|
||||||
if (kbsMasterKey == null) {
|
if (kbsMasterKey == null) {
|
||||||
Log.w(TAG, "No KBS master key is set! Must abort.");
|
Log.w(TAG, "No KBS master key is set! Must abort.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] storageServiceKey = SignalStorageUtil.computeStorageServiceKey(kbsMasterKey);
|
byte[] storageServiceKey = kbsMasterKey.deriveStorageServiceKey();
|
||||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||||
|
@ -29,11 +29,11 @@ import org.thoughtcrime.securesms.util.Util;
|
|||||||
import org.whispersystems.libsignal.InvalidKeyException;
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
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.push.exceptions.PushNetworkException;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -110,14 +110,14 @@ public class StorageSyncJob extends BaseJob {
|
|||||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||||
byte[] kbsMasterKey = SignalStore.kbsValues().getMasterKey();
|
MasterKey kbsMasterKey = SignalStore.kbsValues().getMasterKey();
|
||||||
|
|
||||||
if (kbsMasterKey == null) {
|
if (kbsMasterKey == null) {
|
||||||
Log.w(TAG, "No KBS master key is set! Must abort.");
|
Log.w(TAG, "No KBS master key is set! Must abort.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] storageServiceKey = SignalStorageUtil.computeStorageServiceKey(kbsMasterKey);
|
byte[] storageServiceKey = kbsMasterKey.deriveStorageServiceKey();
|
||||||
boolean needsMultiDeviceSync = false;
|
boolean needsMultiDeviceSync = false;
|
||||||
long localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
|
long localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
|
||||||
SignalStorageManifest remoteManifest = accountManager.getStorageManifest(storageServiceKey).or(new SignalStorageManifest(0, Collections.emptyList()));
|
SignalStorageManifest remoteManifest = accountManager.getStorageManifest(storageServiceKey).or(new SignalStorageManifest(0, Collections.emptyList()));
|
||||||
|
@ -4,6 +4,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
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.contacts.entities.TokenResponse;
|
||||||
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
import org.whispersystems.signalservice.internal.registrationpin.PinStretcher;
|
||||||
|
|
||||||
@ -51,8 +52,13 @@ public final class KbsValues {
|
|||||||
editor.commit();
|
editor.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getMasterKey() {
|
public @Nullable MasterKey getMasterKey() {
|
||||||
return store.getBlob(REGISTRATION_LOCK_MASTER_KEY, null);
|
byte[] blob = store.getBlob(REGISTRATION_LOCK_MASTER_KEY, null);
|
||||||
|
if (blob != null) {
|
||||||
|
return new MasterKey(blob);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable String getRegistrationLockToken() {
|
public @Nullable String getRegistrationLockToken() {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
13
app/src/test/resources/data/kbs_vectors.json
Normal file
13
app/src/test/resources/data/kbs_vectors.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -8,12 +8,15 @@ import javax.crypto.spec.SecretKeySpec;
|
|||||||
|
|
||||||
public final class CryptoUtil {
|
public final class CryptoUtil {
|
||||||
|
|
||||||
private CryptoUtil () { }
|
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||||
|
|
||||||
public static byte[] computeHmacSha256(byte[] key, byte[] data) {
|
private CryptoUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] hmacSha256(byte[] key, byte[] data) {
|
||||||
try {
|
try {
|
||||||
Mac mac = Mac.getInstance("HmacSHA256");
|
Mac mac = Mac.getInstance(HMAC_SHA256);
|
||||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
mac.init(new SecretKeySpec(key, HMAC_SHA256));
|
||||||
return mac.doFinal(data);
|
return mac.doFinal(data);
|
||||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
package org.whispersystems.signalservice.api.crypto;
|
||||||
|
|
||||||
|
import org.whispersystems.util.StringUtil;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static java.util.Arrays.copyOfRange;
|
||||||
|
import static org.whispersystems.signalservice.api.crypto.CryptoUtil.hmacSha256;
|
||||||
|
import static org.whispersystems.util.ByteArrayUtil.concat;
|
||||||
|
import static org.whispersystems.util.ByteArrayUtil.xor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts or decrypts with a Synthetic IV.
|
||||||
|
* <p>
|
||||||
|
* Normal Java casing has been ignored to match original specifications.
|
||||||
|
*/
|
||||||
|
public final class HmacSIV {
|
||||||
|
|
||||||
|
private static final byte[] AUTH_BYTES = StringUtil.utf8("auth");
|
||||||
|
private static final byte[] ENC_BYTES = StringUtil.utf8("enc");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts M with K.
|
||||||
|
*
|
||||||
|
* @param K Key
|
||||||
|
* @param M 32-byte Key to encrypt
|
||||||
|
* @return (IV, C) 48-bytes: 16-byte Synthetic IV and 32-byte Ciphertext.
|
||||||
|
*/
|
||||||
|
public static byte[] encrypt(byte[] K, byte[] M) {
|
||||||
|
if (K.length != 32) throw new AssertionError("K was wrong length");
|
||||||
|
if (M.length != 32) throw new AssertionError("M was wrong length");
|
||||||
|
|
||||||
|
byte[] Ka = hmacSha256(K, AUTH_BYTES);
|
||||||
|
byte[] Ke = hmacSha256(K, ENC_BYTES);
|
||||||
|
byte[] IV = copyOfRange(hmacSha256(Ka, M), 0, 16);
|
||||||
|
byte[] Kx = hmacSha256(Ke, IV);
|
||||||
|
byte[] C = xor(Kx, M);
|
||||||
|
return concat(IV, C);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts M from (IV, C) with K.
|
||||||
|
*
|
||||||
|
* @param K Key
|
||||||
|
* @param IVC Output from {@link #encrypt(byte[], byte[])}
|
||||||
|
* @return 32-byte M
|
||||||
|
* @throws InvalidCiphertextException if the supplied IVC was not correct.
|
||||||
|
*/
|
||||||
|
public static byte[] decrypt(byte[] K, byte[] IVC) throws InvalidCiphertextException {
|
||||||
|
if (K.length != 32) throw new AssertionError("K was wrong length");
|
||||||
|
if (IVC.length != 48) throw new InvalidCiphertextException("IVC was wrong length");
|
||||||
|
|
||||||
|
byte[] IV = copyOfRange(IVC, 0, 16);
|
||||||
|
byte[] C = copyOfRange(IVC, 16, 48);
|
||||||
|
|
||||||
|
byte[] Ka = hmacSha256(K, AUTH_BYTES);
|
||||||
|
byte[] Ke = hmacSha256(K, ENC_BYTES);
|
||||||
|
byte[] Kx = hmacSha256(Ke, IV);
|
||||||
|
byte[] M = xor(Kx, C);
|
||||||
|
|
||||||
|
byte[] eExpectedIV = copyOfRange(hmacSha256(Ka, M), 0, 16);
|
||||||
|
|
||||||
|
if (Arrays.equals(IV, eExpectedIV)) {
|
||||||
|
return M;
|
||||||
|
} else {
|
||||||
|
throw new InvalidCiphertextException("IV was incorrect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a hashed pin, from which you can create or decrypt KBS data.
|
||||||
|
* <p>
|
||||||
|
* Normal Java casing has been ignored to match original specifications.
|
||||||
|
*/
|
||||||
|
public final class HashedPin {
|
||||||
|
|
||||||
|
private final byte[] K;
|
||||||
|
private final byte[] kbsAccessKey;
|
||||||
|
|
||||||
|
private HashedPin(byte[] K, byte[] kbsAccessKey) {
|
||||||
|
this.K = K;
|
||||||
|
this.kbsAccessKey = kbsAccessKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HashedPin fromArgon2Hash(byte[] argon2Hash64) {
|
||||||
|
if (argon2Hash64.length != 64) throw new AssertionError();
|
||||||
|
|
||||||
|
byte[] K = copyOfRange(argon2Hash64, 0, 32);
|
||||||
|
byte[] kbsAccessKey = copyOfRange(argon2Hash64, 32, 64);
|
||||||
|
return new HashedPin(K, kbsAccessKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link KbsData} to store on KBS.
|
||||||
|
*/
|
||||||
|
public KbsData createNewKbsData(SecureRandom random) {
|
||||||
|
byte[] M = new byte[32];
|
||||||
|
random.nextBytes(M);
|
||||||
|
byte[] IVC = HmacSIV.encrypt(K, M);
|
||||||
|
return new KbsData(M, kbsAccessKey, IVC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes 48 byte IVC from KBS and returns full {@link KbsData}.
|
||||||
|
*/
|
||||||
|
public KbsData decryptKbsDataIVCipherText(byte[] IVC) throws InvalidCiphertextException {
|
||||||
|
byte[] masterKey = HmacSIV.decrypt(K, IVC);
|
||||||
|
return new KbsData(masterKey, kbsAccessKey, IVC);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package org.whispersystems.signalservice.api.kbs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct from a {@link HashedPin}.
|
||||||
|
*/
|
||||||
|
public final class KbsData {
|
||||||
|
private final MasterKey masterKey;
|
||||||
|
private final byte[] kbsAccessKey;
|
||||||
|
private final byte[] cipherText;
|
||||||
|
|
||||||
|
KbsData(byte[] masterKey, byte[] kbsAccessKey, byte[] cipherText) {
|
||||||
|
this.masterKey = new MasterKey(masterKey);
|
||||||
|
this.kbsAccessKey = kbsAccessKey;
|
||||||
|
this.cipherText = cipherText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterKey getMasterKey() {
|
||||||
|
return masterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getKbsAccessKey() {
|
||||||
|
return kbsAccessKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getCipherText() {
|
||||||
|
return cipherText;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package org.whispersystems.signalservice.api.kbs;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.util.Hex;
|
||||||
|
import org.whispersystems.util.StringUtil;
|
||||||
|
|
||||||
|
import static org.whispersystems.signalservice.api.crypto.CryptoUtil.hmacSha256;
|
||||||
|
|
||||||
|
public final class MasterKey {
|
||||||
|
|
||||||
|
private final byte[] masterKey;
|
||||||
|
|
||||||
|
public MasterKey(byte[] masterKey) {
|
||||||
|
if (masterKey.length != 32) throw new AssertionError();
|
||||||
|
|
||||||
|
this.masterKey = masterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String deriveRegistrationLock() {
|
||||||
|
return Hex.toStringCondensed(derive("Registration Lock"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] deriveStorageServiceKey() {
|
||||||
|
return derive("Storage Service Encryption");
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] derive(String keyName) {
|
||||||
|
return hmacSha256(masterKey, StringUtil.utf8(keyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] serialize() {
|
||||||
|
return masterKey.clone();
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
package org.whispersystems.signalservice.api.storage;
|
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.crypto.CryptoUtil;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
public final class SignalStorageUtil {
|
|
||||||
|
|
||||||
public static byte[] computeStorageServiceKey(byte[] kbsMasterKey) {
|
|
||||||
return CryptoUtil.computeHmacSha256(kbsMasterKey, "Storage Service Encryption".getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,28 @@
|
|||||||
|
package org.whispersystems.util;
|
||||||
|
|
||||||
|
public final class ByteArrayUtil {
|
||||||
|
|
||||||
|
private ByteArrayUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] xor(byte[] a, byte[] b) {
|
||||||
|
if (a.length != b.length) {
|
||||||
|
throw new AssertionError("XOR length mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] out = new byte[a.length];
|
||||||
|
|
||||||
|
for (int i = a.length - 1; i >= 0; i--) {
|
||||||
|
out[i] = (byte) (a[i] ^ b[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] concat(byte[] a, byte[] b) {
|
||||||
|
byte[] result = new byte[a.length + b.length];
|
||||||
|
System.arraycopy(a, 0, result, 0, a.length);
|
||||||
|
System.arraycopy(b, 0, result, a.length, b.length);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package org.whispersystems.util;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public final class StringUtil {
|
||||||
|
|
||||||
|
private StringUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] utf8(String string) {
|
||||||
|
return string.getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user