Merge "Add HeadsetPiece.java"
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java
new file mode 100644
index 0000000..50a818b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryption.java
@@ -0,0 +1,161 @@
+/*
+ * 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.google.common.primitives.Bytes.concat;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+/**
+ * AES-CTR utilities used for encrypting and decrypting Fast Pair packets that contain multiple
+ * blocks. Encrypts input data by:
+ *
+ * <ol>
+ * <li>encryptedBlock[i] = clearBlock[i] ^ AES(counter), and
+ * <li>concat(encryptedBlock[0], encryptedBlock[1],...) to create the encrypted result, where
+ * <li>counter: the 16-byte input of AES. counter = iv + block_index.
+ * <li>iv: extend 8-byte nonce to 16 bytes with zero padding. i.e. concat(0x0000000000000000,
+ * nonce).
+ * <li>nonce: the cryptographically random 8 bytes, must never be reused with the same key.
+ * </ol>
+ */
+final class AesCtrMultipleBlockEncryption {
+
+ /** Length for AES-128 key. */
+ static final int KEY_LENGTH = AesEcbSingleBlockEncryption.KEY_LENGTH;
+
+ @VisibleForTesting
+ static final int AES_BLOCK_LENGTH = AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
+
+ /** Length of the nonce, a byte array of cryptographically random bytes. */
+ static final int NONCE_SIZE = 8;
+
+ private static final int IV_SIZE = AES_BLOCK_LENGTH;
+ private static final int MAX_NUMBER_OF_BLOCKS = 4;
+
+ private AesCtrMultipleBlockEncryption() {}
+
+ /** Generates a 16-byte AES key. */
+ static byte[] generateKey() throws NoSuchAlgorithmException {
+ return AesEcbSingleBlockEncryption.generateKey();
+ }
+
+ /**
+ * Encrypts data using AES-CTR by the given secret.
+ *
+ * @param secret AES-128 key.
+ * @param data the plaintext to be encrypted.
+ * @return the encrypted data with the 8-byte nonce appended to the front.
+ */
+ static byte[] encrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
+ byte[] nonce = generateNonce();
+ return concat(nonce, doAesCtr(secret, data, nonce));
+ }
+
+ /**
+ * Decrypts data using AES-CTR by the given secret and nonce.
+ *
+ * @param secret AES-128 key.
+ * @param data the first 8 bytes is the nonce, and the remaining is the encrypted data to be
+ * decrypted.
+ * @return the decrypted data.
+ */
+ static byte[] decrypt(byte[] secret, byte[] data) throws GeneralSecurityException {
+ if (data == null || data.length <= NONCE_SIZE) {
+ throw new GeneralSecurityException(
+ "Incorrect data length "
+ + (data == null ? "NULL" : data.length)
+ + " to decrypt, the data should contain nonce.");
+ }
+ byte[] nonce = Arrays.copyOf(data, NONCE_SIZE);
+ byte[] encryptedData = Arrays.copyOfRange(data, NONCE_SIZE, data.length);
+ return doAesCtr(secret, encryptedData, nonce);
+ }
+
+ /**
+ * Generates cryptographically random NONCE_SIZE bytes nonce. This nonce can be used only once.
+ * Always call this function to generate a new nonce before a new encryption.
+ */
+ // Suppression for a warning for potentially insecure random numbers on Android 4.3 and older.
+ // Fast Pair service is only for Android 6.0+ devices.
+ static byte[] generateNonce() {
+ SecureRandom random = new SecureRandom();
+ byte[] nonce = new byte[NONCE_SIZE];
+ random.nextBytes(nonce);
+
+ return nonce;
+ }
+
+ // AES-CTR implementation.
+ @VisibleForTesting
+ static byte[] doAesCtr(byte[] secret, byte[] data, byte[] nonce)
+ throws GeneralSecurityException {
+ if (secret.length != KEY_LENGTH) {
+ throw new IllegalArgumentException(
+ "Incorrect key length for encryption, only supports 16-byte AES Key.");
+ }
+ if (nonce.length != NONCE_SIZE) {
+ throw new IllegalArgumentException(
+ "Incorrect nonce length for encryption, "
+ + "Fast Pair naming scheme only supports 8-byte nonce.");
+ }
+
+ // Keeps the following operations on this byte[], returns it as the final AES-CTR result.
+ byte[] aesCtrResult = new byte[data.length];
+ System.arraycopy(data, /*srcPos=*/ 0, aesCtrResult, /*destPos=*/ 0, data.length);
+
+ // Initializes counter as IV.
+ byte[] counter = createIv(nonce);
+ // The length of the given data is permitted to non-align block size.
+ int numberOfBlocks =
+ (data.length / AES_BLOCK_LENGTH) + ((data.length % AES_BLOCK_LENGTH == 0) ? 0 : 1);
+
+ if (numberOfBlocks > MAX_NUMBER_OF_BLOCKS) {
+ throw new IllegalArgumentException(
+ "Incorrect data size, Fast Pair naming scheme only supports 4 blocks.");
+ }
+
+ for (int i = 0; i < numberOfBlocks; i++) {
+ // Performs the operation: encryptedBlock[i] = clearBlock[i] ^ AES(counter).
+ counter[0] = (byte) (i & 0xFF);
+ byte[] aesOfCounter = doAesSingleBlock(secret, counter);
+ int start = i * AES_BLOCK_LENGTH;
+ // The size of the last block of data may not be 16 bytes. If not, still do xor to the
+ // last byte of data.
+ int end = Math.min(start + AES_BLOCK_LENGTH, data.length);
+ for (int j = 0; start < end; j++, start++) {
+ aesCtrResult[start] ^= aesOfCounter[j];
+ }
+ }
+ return aesCtrResult;
+ }
+
+ private static byte[] doAesSingleBlock(byte[] secret, byte[] counter)
+ throws GeneralSecurityException {
+ return AesEcbSingleBlockEncryption.encrypt(secret, counter);
+ }
+
+ /** Extends 8-byte nonce to 16 bytes with zero padding to create IV. */
+ private static byte[] createIv(byte[] nonce) {
+ return concat(new byte[IV_SIZE - NONCE_SIZE], nonce);
+ }
+}
diff --git a/nearby/tests/src/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryptionTest.java b/nearby/tests/src/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryptionTest.java
new file mode 100644
index 0000000..d17f373
--- /dev/null
+++ b/nearby/tests/src/com/android/server/nearby/common/bluetooth/fastpair/AesCtrMultipleBlockEncryptionTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.AesCtrMultipleBlockEncryption.NONCE_SIZE;
+import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.KEY_LENGTH;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+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 AesCtrMultpleBlockEncryption}. */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AesCtrMultipleBlockEncryptionTest {
+
+ @Test
+ public void decryptEncryptedData_nonBlockSizeAligned_mustEqualToPlaintext() throws Exception {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8); // The length is 31.
+
+ byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+ byte[] decrypted = AesCtrMultipleBlockEncryption.decrypt(secret, encrypted);
+
+ assertThat(decrypted).isEqualTo(plaintext);
+ }
+
+ @Test
+ public void decryptEncryptedData_blockSizeAligned_mustEqualToPlaintext() throws Exception {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext =
+ // The length is 32.
+ base16().decode("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF");
+
+ byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+ byte[] decrypted = AesCtrMultipleBlockEncryption.decrypt(secret, encrypted);
+
+ assertThat(decrypted).isEqualTo(plaintext);
+ }
+
+ @Test
+ public void generateNonceTwice_mustBeDifferent() {
+ byte[] nonce1 = AesCtrMultipleBlockEncryption.generateNonce();
+ byte[] nonce2 = AesCtrMultipleBlockEncryption.generateNonce();
+
+ assertThat(nonce1).isNotEqualTo(nonce2);
+ }
+
+ @Test
+ public void encryptedSamePlaintext_mustBeDifferentEncryptedResult() throws Exception {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ byte[] encrypted1 = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+ byte[] encrypted2 = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+
+ assertThat(encrypted1).isNotEqualTo(encrypted2);
+ }
+
+ @Test
+ public void encryptData_mustBeDifferentToUnencrypted() throws Exception {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, plaintext);
+
+ assertThat(encrypted).isNotEqualTo(plaintext);
+ }
+
+ @Test
+ public void inputIncorrectKeySizeToEncrypt_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH + 1];
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AesCtrMultipleBlockEncryption.encrypt(secret, plaintext));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Incorrect key length for encryption, only supports 16-byte AES Key.");
+ }
+
+ @Test
+ public void inputIncorrectKeySizeToDecrypt_mustThrowException() {
+ byte[] secret = new byte[KEY_LENGTH - 1];
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AesCtrMultipleBlockEncryption.decrypt(secret, plaintext));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Incorrect key length for encryption, only supports 16-byte AES Key.");
+ }
+
+ @Test
+ public void inputIncorrectDataSizeToDecrypt_mustThrowException()
+ throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] plaintext = "Someone's Google Headphone 2019".getBytes(UTF_8);
+
+ byte[] encryptedData = Arrays.copyOfRange(
+ AesCtrMultipleBlockEncryption.encrypt(secret, plaintext), /*from=*/ 0, NONCE_SIZE);
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AesCtrMultipleBlockEncryption.decrypt(secret, encryptedData));
+
+ assertThat(exception).hasMessageThat().contains("Incorrect data length");
+ }
+
+ // Add some random tests that for a certain amount of random plaintext of random length to prove
+ // our encryption/decryption is correct. This is suggested by security team.
+ @Test
+ public void decryptEncryptedRandomDataForCertainAmount_mustEqualToOriginalData()
+ throws Exception {
+ SecureRandom random = new SecureRandom();
+ for (int i = 0; i < 1000; i++) {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ int dataLength = random.nextInt(64) + 1;
+ byte[] data = new byte[dataLength];
+ random.nextBytes(data);
+
+ byte[] encrypted = AesCtrMultipleBlockEncryption.encrypt(secret, data);
+ byte[] decrypted = AesCtrMultipleBlockEncryption.decrypt(secret, encrypted);
+
+ assertThat(decrypted).isEqualTo(data);
+ }
+ }
+
+ // Add some random tests that for a certain amount of random plaintext of random length to prove
+ // our encryption is correct. This is suggested by security team.
+ @Test
+ public void twoDistinctEncryptionOnSameRandomData_mustBeDifferentResult() throws Exception {
+ SecureRandom random = new SecureRandom();
+ for (int i = 0; i < 1000; i++) {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ int dataLength = random.nextInt(64) + 1;
+ byte[] data = new byte[dataLength];
+ random.nextBytes(data);
+
+ byte[] encrypted1 = AesCtrMultipleBlockEncryption.encrypt(secret, data);
+ byte[] encrypted2 = AesCtrMultipleBlockEncryption.encrypt(secret, data);
+
+ assertThat(encrypted1).isNotEqualTo(encrypted2);
+ }
+ }
+
+ // Adds this test example on spec. Also we can easily change the parameters(e.g. secret, data,
+ // nonce) to clarify test results with partners.
+ @Test
+ public void inputTestExampleToEncrypt_getCorrectResult() throws GeneralSecurityException {
+ byte[] secret = base16().decode("0123456789ABCDEF0123456789ABCDEF");
+ byte[] nonce = base16().decode("0001020304050607");
+
+ // "Someone's Google Headphone".getBytes(UTF_8) is
+ // base16().decode("536F6D656F6E65277320476F6F676C65204865616470686F6E65");
+ byte[] encryptedData =
+ AesCtrMultipleBlockEncryption.doAesCtr(
+ secret,
+ "Someone's Google Headphone".getBytes(UTF_8),
+ nonce);
+
+ assertThat(encryptedData)
+ .isEqualTo(base16().decode("EE4A2483738052E44E9B2A145E5DDFAA44B9E5536AF438E1E5C6"));
+ }
+}