Add MessageStreamHmacEncoder.
Test: unit test
Bug: 200231384
Change-Id: I4c2fc4fc3c07eb4ad010c2e944f5b43fcf758996
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java
new file mode 100644
index 0000000..b04cf73
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoder.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.generateNonce;
+
+import static com.google.common.primitives.Bytes.concat;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * Message stream utilities for encoding raw packet with HMAC.
+ *
+ * <p>Encoded packet is:
+ *
+ * <ol>
+ * <li>Packet[0 - (data length - 1)]: the raw data.
+ * <li>Packet[data length - (data length + 7)]: the 8-byte message nonce.
+ * <li>Packet[(data length + 8) - (data length + 15)]: the 8-byte of HMAC.
+ * </ol>
+ */
+public class MessageStreamHmacEncoder {
+ public static final int EXTRACT_HMAC_SIZE = 8;
+ public static final int SECTION_NONCE_LENGTH = 8;
+
+ private MessageStreamHmacEncoder() {}
+
+ /** Encodes Message Packet. */
+ public static byte[] encodeMessagePacket(byte[] accountKey, byte[] sectionNonce, byte[] data)
+ throws GeneralSecurityException {
+ checkAccountKeyAndSectionNonce(accountKey, sectionNonce);
+
+ if (data == null || data.length == 0) {
+ throw new GeneralSecurityException("No input data for encodeMessagePacket");
+ }
+
+ byte[] messageNonce = generateNonce();
+ byte[] extractedHmac =
+ Arrays.copyOf(
+ HmacSha256.buildWith64BytesKey(
+ accountKey, concat(sectionNonce, messageNonce, data)),
+ EXTRACT_HMAC_SIZE);
+
+ return concat(data, messageNonce, extractedHmac);
+ }
+
+ /** Verifies Hmac. */
+ public static boolean verifyHmac(byte[] accountKey, byte[] sectionNonce, byte[] data)
+ throws GeneralSecurityException {
+ checkAccountKeyAndSectionNonce(accountKey, sectionNonce);
+ if (data == null) {
+ throw new GeneralSecurityException("data is null");
+ }
+ if (data.length <= EXTRACT_HMAC_SIZE + SECTION_NONCE_LENGTH) {
+ throw new GeneralSecurityException("data.length too short");
+ }
+
+ byte[] hmac = Arrays.copyOfRange(data, data.length - EXTRACT_HMAC_SIZE, data.length);
+ byte[] messageNonce =
+ Arrays.copyOfRange(
+ data,
+ data.length - EXTRACT_HMAC_SIZE - SECTION_NONCE_LENGTH,
+ data.length - EXTRACT_HMAC_SIZE);
+ byte[] rawData = Arrays.copyOf(
+ data, data.length - EXTRACT_HMAC_SIZE - SECTION_NONCE_LENGTH);
+ return Arrays.equals(
+ Arrays.copyOf(
+ HmacSha256.buildWith64BytesKey(
+ accountKey, concat(sectionNonce, messageNonce, rawData)),
+ EXTRACT_HMAC_SIZE),
+ hmac);
+ }
+
+ private static void checkAccountKeyAndSectionNonce(byte[] accountKey, byte[] sectionNonce)
+ throws GeneralSecurityException {
+ if (accountKey == null || accountKey.length == 0) {
+ throw new GeneralSecurityException(
+ "Incorrect accountKey for encoding message packet, accountKey.length = "
+ + (accountKey == null ? "NULL" : accountKey.length));
+ }
+
+ if (sectionNonce == null || sectionNonce.length != SECTION_NONCE_LENGTH) {
+ throw new GeneralSecurityException(
+ "Incorrect sectionNonce for encoding message packet, sectionNonce.length = "
+ + (sectionNonce == null ? "NULL" : sectionNonce.length));
+ }
+ }
+}
diff --git a/nearby/tests/src/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoderTest.java b/nearby/tests/src/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoderTest.java
new file mode 100644
index 0000000..84c2386
--- /dev/null
+++ b/nearby/tests/src/com/android/server/nearby/common/bluetooth/fastpair/MessageStreamHmacEncoderTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+
+import static com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder.EXTRACT_HMAC_SIZE;
+import static com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder.SECTION_NONCE_LENGTH;
+
+import static com.google.common.primitives.Bytes.concat;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link MessageStreamHmacEncoder}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MessageStreamHmacEncoderTest {
+
+ private static final int ACCOUNT_KEY_LENGTH = 16;
+
+ @Test
+ public void encodeMessagePacket() throws GeneralSecurityException {
+ int messageLength = 2;
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
+ secureRandom.nextBytes(accountKey);
+ byte[] data = new byte[messageLength];
+ secureRandom.nextBytes(data);
+ byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
+ secureRandom.nextBytes(sectionNonce);
+
+ byte[] result = MessageStreamHmacEncoder
+ .encodeMessagePacket(accountKey, sectionNonce, data);
+
+ assertThat(result).hasLength(messageLength + SECTION_NONCE_LENGTH + EXTRACT_HMAC_SIZE);
+ // First bytes are raw message bytes.
+ assertThat(Arrays.copyOf(result, messageLength)).isEqualTo(data);
+ // Following by message nonce.
+ byte[] messageNonce =
+ Arrays.copyOfRange(result, messageLength, messageLength + SECTION_NONCE_LENGTH);
+ byte[] extractedHmac =
+ Arrays.copyOf(
+ HmacSha256.buildWith64BytesKey(accountKey,
+ concat(sectionNonce, messageNonce, data)),
+ EXTRACT_HMAC_SIZE);
+ // Finally hash mac.
+ assertThat(Arrays.copyOfRange(result, messageLength + SECTION_NONCE_LENGTH, result.length))
+ .isEqualTo(extractedHmac);
+ }
+
+ @Test
+ public void verifyHmac() throws GeneralSecurityException {
+ int messageLength = 2;
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
+ secureRandom.nextBytes(accountKey);
+ byte[] data = new byte[messageLength];
+ secureRandom.nextBytes(data);
+ byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
+ secureRandom.nextBytes(sectionNonce);
+ byte[] result = MessageStreamHmacEncoder
+ .encodeMessagePacket(accountKey, sectionNonce, data);
+
+ assertThat(MessageStreamHmacEncoder.verifyHmac(accountKey, sectionNonce, result)).isTrue();
+ }
+
+ @Test
+ public void verifyHmac_failedByAccountKey() throws GeneralSecurityException {
+ int messageLength = 2;
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
+ secureRandom.nextBytes(accountKey);
+ byte[] data = new byte[messageLength];
+ secureRandom.nextBytes(data);
+ byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
+ secureRandom.nextBytes(sectionNonce);
+ byte[] result = MessageStreamHmacEncoder
+ .encodeMessagePacket(accountKey, sectionNonce, data);
+ secureRandom.nextBytes(accountKey);
+
+ assertThat(MessageStreamHmacEncoder.verifyHmac(accountKey, sectionNonce, result)).isFalse();
+ }
+
+ @Test
+ public void verifyHmac_failedBySectionNonce() throws GeneralSecurityException {
+ int messageLength = 2;
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] accountKey = new byte[ACCOUNT_KEY_LENGTH];
+ secureRandom.nextBytes(accountKey);
+ byte[] data = new byte[messageLength];
+ secureRandom.nextBytes(data);
+ byte[] sectionNonce = new byte[SECTION_NONCE_LENGTH];
+ secureRandom.nextBytes(sectionNonce);
+ byte[] result = MessageStreamHmacEncoder
+ .encodeMessagePacket(accountKey, sectionNonce, data);
+ secureRandom.nextBytes(sectionNonce);
+
+ assertThat(MessageStreamHmacEncoder.verifyHmac(accountKey, sectionNonce, result)).isFalse();
+ }
+}