blob: 1521be6033871485e2ed85c1048bf453a00538bf [file] [log] [blame]
/*
* 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.NONCE_SIZE;
import static com.google.common.primitives.Bytes.concat;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.annotation.TargetApi;
import android.os.Build.VERSION_CODES;
import com.google.common.base.Utf8;
import java.security.GeneralSecurityException;
import java.util.Arrays;
/**
* Naming utilities for encoding naming packet, decoding naming packet and verifying both the data
* integrity and the authentication of a message by checking HMAC.
*
* <p>Naming packet is:
*
* <ol>
* <li>Naming_Packet[0 - 7]: the first 8-byte of HMAC.
* <li>Naming_Packet[8 - var]: the encrypted name (with 8-byte nonce appended to the front).
* </ol>
*/
@TargetApi(VERSION_CODES.M)
public final class NamingEncoder {
static final int EXTRACT_HMAC_SIZE = 8;
static final int MAX_LENGTH_OF_NAME = 48;
private NamingEncoder() {
}
/**
* Encodes the name to naming packet by the given secret.
*
* @param secret AES-128 key for encryption.
* @param name the given name to be encoded.
* @return the encrypted data with the 8-byte extracted HMAC appended to the front.
* @throws GeneralSecurityException if the given key or name is invalid for encoding.
*/
public static byte[] encodeNamingPacket(byte[] secret, String name)
throws GeneralSecurityException {
if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
throw new GeneralSecurityException(
"Incorrect secret for encoding name packet, secret.length = "
+ (secret == null ? "NULL" : secret.length));
}
if ((name == null) || (name.length() == 0) || (Utf8.encodedLength(name)
> MAX_LENGTH_OF_NAME)) {
throw new GeneralSecurityException(
"Invalid name for encoding name packet, Utf8.encodedLength(name) = "
+ (name == null ? "NULL" : Utf8.encodedLength(name)));
}
byte[] encryptedData = AesCtrMultipleBlockEncryption.encrypt(secret, name.getBytes(UTF_8));
byte[] extractedHmac =
Arrays.copyOf(HmacSha256.build(secret, encryptedData), EXTRACT_HMAC_SIZE);
return concat(extractedHmac, encryptedData);
}
/**
* Decodes the name from naming packet by the given secret.
*
* @param secret AES-128 key used in the encryption to decrypt data.
* @param namingPacket naming packet which is encoded by the given secret..
* @return the name decoded from the given packet.
* @throws GeneralSecurityException if the given key or naming packet is invalid for decoding.
*/
public static String decodeNamingPacket(byte[] secret, byte[] namingPacket)
throws GeneralSecurityException {
if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) {
throw new GeneralSecurityException(
"Incorrect secret for decoding name packet, secret.length = "
+ (secret == null ? "NULL" : secret.length));
}
if (namingPacket == null
|| namingPacket.length <= EXTRACT_HMAC_SIZE
|| namingPacket.length > (MAX_LENGTH_OF_NAME + EXTRACT_HMAC_SIZE + NONCE_SIZE)) {
throw new GeneralSecurityException(
"Naming packet size is incorrect, namingPacket.length is "
+ (namingPacket == null ? "NULL" : namingPacket.length));
}
if (!verifyHmac(secret, namingPacket)) {
throw new GeneralSecurityException(
"Verify HMAC failed, could be incorrect key or naming packet.");
}
byte[] encryptedData = Arrays
.copyOfRange(namingPacket, EXTRACT_HMAC_SIZE, namingPacket.length);
return new String(AesCtrMultipleBlockEncryption.decrypt(secret, encryptedData), UTF_8);
}
// Computes the HMAC of the given key and name, and compares the first 8-byte of the HMAC result
// with the one from name packet. Must call constant-time comparison to prevent a possible
// timing attack, e.g. time the same MAC with all different first byte for a given ciphertext,
// the right one will take longer as it will fail on the second byte's verification.
private static boolean verifyHmac(byte[] key, byte[] namingPacket)
throws GeneralSecurityException {
byte[] packetHmac = Arrays.copyOfRange(namingPacket, /* from= */ 0, EXTRACT_HMAC_SIZE);
byte[] encryptedData = Arrays
.copyOfRange(namingPacket, EXTRACT_HMAC_SIZE, namingPacket.length);
byte[] computedHmac = Arrays
.copyOf(HmacSha256.build(key, encryptedData), EXTRACT_HMAC_SIZE);
return HmacSha256.compareTwoHMACs(packetHmac, computedHmac);
}
}