From 7d70ea78cd848bb924d6b42fcb0df9a429fb905f Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Fri, 17 Jan 2020 13:31:30 -0500 Subject: [PATCH] Hmac-SIV encryption/decryption. --- .../jobs/MultiDeviceKeysUpdateJob.java | 6 +- .../securesms/jobs/StorageForcePushJob.java | 6 +- .../securesms/jobs/StorageSyncJob.java | 6 +- .../securesms/keyvalue/KbsValues.java | 10 ++- .../registration/v2/HashedPinKbsDataTest.java | 58 ++++++++++++++++ .../registration/v2/KbsTestVector.java | 63 +++++++++++++++++ .../registration/v2/KbsTestVectorList.java | 15 ++++ .../securesms/testutil/HexDeserializer.java | 20 ++++++ .../testutil/SecureRandomTestUtil.java | 48 +++++++++++++ app/src/test/resources/data/kbs_vectors.json | 13 ++++ .../signalservice/api/crypto/CryptoUtil.java | 11 +-- .../signalservice/api/crypto/HmacSIV.java | 69 +++++++++++++++++++ .../signalservice/api/kbs/HashedPin.java | 50 ++++++++++++++ .../signalservice/api/kbs/KbsData.java | 28 ++++++++ .../signalservice/api/kbs/MasterKey.java | 33 +++++++++ .../api/storage/SignalStorageUtil.java | 13 ---- .../whispersystems/util/ByteArrayUtil.java | 28 ++++++++ .../org/whispersystems/util/StringUtil.java | 13 ++++ 18 files changed, 462 insertions(+), 28 deletions(-) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVector.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVectorList.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/testutil/HexDeserializer.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/testutil/SecureRandomTestUtil.java create mode 100644 app/src/test/resources/data/kbs_vectors.json create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/HmacSIV.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java delete mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageUtil.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/util/ByteArrayUtil.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/util/StringUtil.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java index 9fff09d11f..47370b3871 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java index fb7686b73c..aaecc22743 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java index 141e6de6c6..b783af23a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java @@ -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())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java index 45e3727f9f..2eca7ac4bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java @@ -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() { diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java new file mode 100644 index 0000000000..17f1a24c62 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/HashedPinKbsDataTest.java @@ -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; + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVector.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVector.java new file mode 100644 index 0000000000..9eb1715fe4 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVector.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVectorList.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVectorList.java new file mode 100644 index 0000000000..933149bdde --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/KbsTestVectorList.java @@ -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 vectors; + + public List getVectors() { + return vectors; + } +} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/HexDeserializer.java b/app/src/test/java/org/thoughtcrime/securesms/testutil/HexDeserializer.java new file mode 100644 index 0000000000..6bae85da8f --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/HexDeserializer.java @@ -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 { + + @Override + public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return Hex.fromStringCondensed(p.getText()); + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/SecureRandomTestUtil.java b/app/src/test/java/org/thoughtcrime/securesms/testutil/SecureRandomTestUtil.java new file mode 100644 index 0000000000..3cb7526e27 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/SecureRandomTestUtil.java @@ -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. + *

+ * 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 argument = ArgumentCaptor.forClass(byte[].class); + + doAnswer(new Answer() { + + 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; + } +} diff --git a/app/src/test/resources/data/kbs_vectors.json b/app/src/test/resources/data/kbs_vectors.json new file mode 100644 index 0000000000..e32c5d9492 --- /dev/null +++ b/app/src/test/resources/data/kbs_vectors.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/CryptoUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/CryptoUtil.java index 0d4e2861b8..8862f43d35 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/CryptoUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/CryptoUtil.java @@ -8,12 +8,15 @@ import javax.crypto.spec.SecretKeySpec; 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 { - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(new SecretKeySpec(key, "HmacSHA256")); + Mac mac = Mac.getInstance(HMAC_SHA256); + mac.init(new SecretKeySpec(key, HMAC_SHA256)); return mac.doFinal(data); } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new AssertionError(e); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/HmacSIV.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/HmacSIV.java new file mode 100644 index 0000000000..74f58d1a78 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/HmacSIV.java @@ -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. + *

+ * 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"); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java new file mode 100644 index 0000000000..409f6a0f0c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/HashedPin.java @@ -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. + *

+ * 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); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java new file mode 100644 index 0000000000..eb76d2bf47 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/KbsData.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java new file mode 100644 index 0000000000..5add74dc15 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java @@ -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(); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageUtil.java deleted file mode 100644 index aa42fd930b..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageUtil.java +++ /dev/null @@ -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)); - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/util/ByteArrayUtil.java b/libsignal/service/src/main/java/org/whispersystems/util/ByteArrayUtil.java new file mode 100644 index 0000000000..460994391c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/util/ByteArrayUtil.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/util/StringUtil.java b/libsignal/service/src/main/java/org/whispersystems/util/StringUtil.java new file mode 100644 index 0000000000..6a20e4c9d1 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/util/StringUtil.java @@ -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); + } +}