1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.nearby.common.bluetooth.fastpair; 18 19 import static com.android.server.nearby.common.bluetooth.fastpair.AesCtrMultipleBlockEncryption.NONCE_SIZE; 20 21 import static com.google.common.primitives.Bytes.concat; 22 23 import static java.nio.charset.StandardCharsets.UTF_8; 24 25 import android.annotation.TargetApi; 26 import android.os.Build.VERSION_CODES; 27 28 import com.google.common.base.Utf8; 29 30 import java.security.GeneralSecurityException; 31 import java.util.Arrays; 32 33 /** 34 * Naming utilities for encoding naming packet, decoding naming packet and verifying both the data 35 * integrity and the authentication of a message by checking HMAC. 36 * 37 * <p>Naming packet is: 38 * 39 * <ol> 40 * <li>Naming_Packet[0 - 7]: the first 8-byte of HMAC. 41 * <li>Naming_Packet[8 - var]: the encrypted name (with 8-byte nonce appended to the front). 42 * </ol> 43 */ 44 @TargetApi(VERSION_CODES.M) 45 public final class NamingEncoder { 46 47 static final int EXTRACT_HMAC_SIZE = 8; 48 static final int MAX_LENGTH_OF_NAME = 48; 49 NamingEncoder()50 private NamingEncoder() { 51 } 52 53 /** 54 * Encodes the name to naming packet by the given secret. 55 * 56 * @param secret AES-128 key for encryption. 57 * @param name the given name to be encoded. 58 * @return the encrypted data with the 8-byte extracted HMAC appended to the front. 59 * @throws GeneralSecurityException if the given key or name is invalid for encoding. 60 */ encodeNamingPacket(byte[] secret, String name)61 public static byte[] encodeNamingPacket(byte[] secret, String name) 62 throws GeneralSecurityException { 63 if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) { 64 throw new GeneralSecurityException( 65 "Incorrect secret for encoding name packet, secret.length = " 66 + (secret == null ? "NULL" : secret.length)); 67 } 68 69 if ((name == null) || (name.length() == 0) || (Utf8.encodedLength(name) 70 > MAX_LENGTH_OF_NAME)) { 71 throw new GeneralSecurityException( 72 "Invalid name for encoding name packet, Utf8.encodedLength(name) = " 73 + (name == null ? "NULL" : Utf8.encodedLength(name))); 74 } 75 76 byte[] encryptedData = AesCtrMultipleBlockEncryption.encrypt(secret, name.getBytes(UTF_8)); 77 byte[] extractedHmac = 78 Arrays.copyOf(HmacSha256.build(secret, encryptedData), EXTRACT_HMAC_SIZE); 79 80 return concat(extractedHmac, encryptedData); 81 } 82 83 /** 84 * Decodes the name from naming packet by the given secret. 85 * 86 * @param secret AES-128 key used in the encryption to decrypt data. 87 * @param namingPacket naming packet which is encoded by the given secret.. 88 * @return the name decoded from the given packet. 89 * @throws GeneralSecurityException if the given key or naming packet is invalid for decoding. 90 */ decodeNamingPacket(byte[] secret, byte[] namingPacket)91 public static String decodeNamingPacket(byte[] secret, byte[] namingPacket) 92 throws GeneralSecurityException { 93 if (secret == null || secret.length != AesCtrMultipleBlockEncryption.KEY_LENGTH) { 94 throw new GeneralSecurityException( 95 "Incorrect secret for decoding name packet, secret.length = " 96 + (secret == null ? "NULL" : secret.length)); 97 } 98 if (namingPacket == null 99 || namingPacket.length <= EXTRACT_HMAC_SIZE 100 || namingPacket.length > (MAX_LENGTH_OF_NAME + EXTRACT_HMAC_SIZE + NONCE_SIZE)) { 101 throw new GeneralSecurityException( 102 "Naming packet size is incorrect, namingPacket.length is " 103 + (namingPacket == null ? "NULL" : namingPacket.length)); 104 } 105 106 if (!verifyHmac(secret, namingPacket)) { 107 throw new GeneralSecurityException( 108 "Verify HMAC failed, could be incorrect key or naming packet."); 109 } 110 byte[] encryptedData = Arrays 111 .copyOfRange(namingPacket, EXTRACT_HMAC_SIZE, namingPacket.length); 112 return new String(AesCtrMultipleBlockEncryption.decrypt(secret, encryptedData), UTF_8); 113 } 114 115 // Computes the HMAC of the given key and name, and compares the first 8-byte of the HMAC result 116 // with the one from name packet. Must call constant-time comparison to prevent a possible 117 // timing attack, e.g. time the same MAC with all different first byte for a given ciphertext, 118 // the right one will take longer as it will fail on the second byte's verification. verifyHmac(byte[] key, byte[] namingPacket)119 private static boolean verifyHmac(byte[] key, byte[] namingPacket) 120 throws GeneralSecurityException { 121 byte[] packetHmac = Arrays.copyOfRange(namingPacket, /* from= */ 0, EXTRACT_HMAC_SIZE); 122 byte[] encryptedData = Arrays 123 .copyOfRange(namingPacket, EXTRACT_HMAC_SIZE, namingPacket.length); 124 byte[] computedHmac = Arrays 125 .copyOf(HmacSha256.build(key, encryptedData), EXTRACT_HMAC_SIZE); 126 127 return HmacSha256.compareTwoHMACs(packetHmac, computedHmac); 128 } 129 } 130