Huawei Link protocol v2 - quantifiedstudent/mobile-android GitHub Wiki
Services and commands
Services from the Huawei Link protocol are different from the services from Bluetooth GATT. The Huawei services are for internal use within the protocol and are used to access services from within the peripheral. There are currently five known services, of which four have been identified. However, it is likely that there are more services which can be used for numerous applications.
Id | Name |
---|---|
1 | DeviceConfig |
2 | Notification |
7 | Fitness |
12 | LocaleConfig |
15 | ? |
Packet structure
A packet from the Huawei Link Protocol consists of multiple components, namely the length, command and checksum.
Every packet starts with a magic value 0x5A
, which may be used to identify the packet as a Huawei Link Protocol packet.
However, this does not verify the packet's authenticity.
# | Field | Field | Type | Length (bytes) |
---|---|---|---|---|
1 | MAGIC | Magic value: 0x5A | byte | 1 |
2 | LEN | length(BODY) + length(CONST) | uint16 | 2 |
3 | CONST | Constant value: 0x00 | byte | 1 |
4 | CMD | Command | Command | <= 65530 |
5 | CRC16 | CRC-16/XMODEM | uint16 | 2 |
Like shown in the table above, a packet is ordered in the following way: 0x5A LEN 0x00 CMD CRC16
.
Command
A command is made up of three components: the service id, command id and one or more TLV's (short for Tag Length Value).
# | Field | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | SS | Service ID | byte | 1 |
2 | CC | Command ID | byte | 1 |
3 | TLVn | TLV | TLV[] | >= 2 |
SS CC TLV1 TLV2 TLV3 TLV...
TLV
TLV (type-length-value or tag-length-value) is an encoding scheme used for optional informational elements in the protocol. A TLV-encoded data stream contains code related to the record identifier, the record value's length, and finally the value itself. Some services are known to use raw data streams, which are not wrapped in TLVs.
# | Field | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | TAG | Tag ID | byte | 1 |
2 | LEN | length(VAL) | VarInt | 2 |
3 | VAL | Value | byte[] | * |
TAG LEN VAL
Checksum
A checksum is a small-sized block of data derived from another block of digital data to detect errors that may have been introduced during its transmission or storage. By themselves, checksums are often used to verify data integrity but are not relied upon to verify data authenticity.
Huawei uses a CRC, short for Cyclic Redundancy Check, for calculating the checksum for their packets.
In particular, the CRC-16/XMODEM
-algorithm is used, which uses 0x1021
as the polynomial, 0x0000
as the initial value and 0x0000
as the XOR value, this finally results in a int16
value as the checksum.
The following shows snippet for calculating the CRC-16 checksum in Kotlin:
override fun calculate(
poly: Int,
init: Int,
data: ByteArray,
offset: Int,
length: Int,
refIn: Boolean,
refOut: Boolean,
xorOut: Int
): Int {
var crc = init
var i = offset
while (i < offset + length && i < data.size) {
val b = data[i]
for (j in 0..7) {
val k = if (refIn) 7 - j else j
val bit = b.toInt() shr 7 - k and 1 == 1
val c15 = crc shr 15 and 1 == 1
crc = crc shl 1
if (c15 xor bit) crc = crc xor poly
}
++i
}
return when {
refOut -> Integer.reverse(crc) shr 16 xor xorOut
else -> crc xor xorOut and 0xFFFF
}
}
Device config
The commands for this service are:
ID | Name |
---|---|
1 | LinkParams |
2 | SupportedServices |
3 | SupportedCommands |
4 | SetDateFormat |
5 | SetTime |
7 | ProductType |
8 | BatteryLevel |
9 | ActivateOnRotate |
11 | ? |
12 | ? |
13 | FactoryReset |
14 | Bond |
15 | BondParams |
18 | ? |
19 | Auth |
26 | LeftRightWrist |
27 | NavigateOnRotate |
Handshake
A handshake consists of the following four steps:
- Bluetooth link parameter negotiation
- Challenge-response authentication
- Bonding/pairing parameters negotiation
- (Optional) Bonding/pairing if not already bonded
Bluetooth link parameters
The first step of the handshake is the Bluetooth link parameter negotiation. This step starts with the Bluetooth link parameter request packet. This packet is sent from the client (e.g., mobile application) to the peripheral (smartwatch). The packet itself does not contain any specific data, only empty TLV's, which tell the peripheral what data to send in return.
# | Tag | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | 0x01 | Protocol version | 0 | |
2 | 0x02 | Maximum frame size | 0 | |
3 | 0x03 | Maximum link size | 0 | |
4 | 0x04 | Connection interval | 0 |
An example of the Bluetooth link parameter request packet: 5a 00 0b 00 01 01 01 00 02 00 03 00 04 00 f1 3b
The packet description describes the default packet sent from the Huawei Health mobile application. In this case, the client requests the protocol version, maximum frame size, maximum link size, and connection interval.
After sending the Bluetooth to link parameter negotiation packet, the peripheral responds with the requested data, including its authentication version and cryptographic nonce. The authentication version and cryptographic nonce are used in the next stage of the handshake for computing the digest challenge.
# | Tag | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | 0x01 | Protocol version | VarInt | >= 1 |
2 | 0x02 | Maximum frame size | VarInt | >= 1 |
3 | 0x03 | Maximum link size | VarInt | >= 1 |
4 | 0x04 | Connection interval | VarInt | >= 1 |
5 | 0x05 | Device nonce | AUTH_VER + NONCE | 2 + 16 |
An example of the Bluetooth link parameter response packet: 5a 00 26 00 01 01 01 01 02 02 02 00 f4 03 02 00 f4 04 02 00 00 05 12 00 01 bb 7b eb d2 9c 13 03 eb 53 2c c6 9b d1 57 ed 0c 8c ea
Challenge-response authentication
The second step of the handshake is the challenge-response authentication. This step starts with the digest challenge packet. This packet contains the client nonce and digest challenge and is sent from the client to the Huawei peripheral. This packet is used to set up the authentic parameters to allow for safely encrypted communication with the peripheral.
Message hashing
The first requirement for computing a digest challenge is the ability to hash messages. In the case of the Huawei protocol, messages may be hashed using the HMAC SHA-256 algorithm. In cryptography, an HMAC (sometimes expanded as either keyed-hash message authentication code or hash-based message authentication code) is a specific type of message authentication code (MAC) involving a cryptographic hash function and a secret cryptographic key. As with any MAC, it may serve to simultaneously verify both the data integrity and authenticity of a message.
The first step of producing a hash for a message is concatenating the peripheral nonce with the client nonce, referred to as the combined nonce. In addition to that, the constant defined digest secret is combined with the input data, referred to as the secret data. After that, a key is computed by calling the HMAC digest function, with the secret data as the key and combined nonce as the data.
Finally, the message hash is computed by calling the HMAC digest function with the key from earlier and the combined nonce as the data. The process can be seen in the code snippet below.
private val DIGEST_SECRET = byteArrayOf(112, -5, 108, 36, 3, 95, -37, 85, 47, 56, -119, -118, -18, -34, 63, 105)
fun computeDigest(
data: ByteArray,
clientNonce: ByteArray,
peripheralNonce: ByteArray
): ByteArray {
val nonce = peripheralNonce + clientNonce
val key = hmac.digest(DIGEST_SECRET + data, nonce)
return hmac.digest(key, nonce)
}
Digest challenge
The digest challenge contained within the packet is computed by calling the previously mentioned compute digest function with both the client nonce and peripheral nonce and entering 0x01 0x00
as the given data.
The process can be seen in the code snippet below.
// import the function from before
fun computeDigestChallenge(
clientNonce: ByteArray,
peripheralNonce: ByteArray
): ByteArray {
return computeDigest(byteArrayOf(1, 0), clientNonce, peripheralNonce)
}
Client nonce
In addition to the computation of the digest challenge, a cryptographic nonce is computed on the client-side. This cryptographic nonce allows for two-way authenticity and encrypted communication. A code snippet of the computation is shown below.
fun generateNonce(): ByteArray {
val bytes = ByteArray(16)
SecureRandom().nextBytes(bytes)
return bytes
}
# | Tag | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | 0x01 | Digest challenge | byte[] | 32 |
2 | 0x02 | Client nonce | AUTH_VER + NONCE | 2 + 16 |
An example of the digest challenge packet: 5a 00 39 00 01 13 01 20 62 0c d2 d1 64 d4 e0 c3 45 94 37 58 0d f3 01 d0 e2 55 a4 b4 b6 4d 7b 92 42 4a b2 72 67 5e a4 35 02 12 00 01 e2 10 fd 2f c2 4c a8 e8 0c 9b c7 1c 47 2f 8a 7f 39 26
After computing the digest challenge and client nonce, the authentication initiation packet is generated in which said values are given. This packet can finally be sent from the client to the Huawei peripheral.
After sending the authentication challenge packet to the peripheral, the peripheral sends a digest response packet. The response packet contains only the digest response, calculated by the peripheral using the given digest challenge and client nonce.
# | Tag | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | 0x01 | Digest response | byte[] | 32 |
An example of the digest response packet: 5a 00 25 00 01 13 01 20 8a 8e 0b 4d 4c 02 ad 2d 5b 98 fb c6 97 1e 09 57 f7 78 d0 cb 3e 01 64 73 d4 44 41 a4 35 e8 3c 7f 02 cb
The digest response required verification to ensure that the HMAC authentication negotiation went correctly on both the client and peripheral sides.
The digest response is verified by computing a local digest response and checking whether it equals the digest response received from the peripheral.
The local digest response is computed, by calling the said digest function the same way as computing the digest challenge.
However, instead of using 0x01 0x00
as the given data, 0x01 0x10
should be used as the given data.
The process can be seen in the code snippet below.
// import the function from before
fun computeDigestResponse(
clientNonce: ByteArray,
peripheralNonce: ByteArray
): ByteArray {
return computeDigest(byteArrayOf(1, 16), clientNonce, peripheralNonce)
}
Bonding/pairing parameters
The third step of the handshake is the bonding parameter negotiation. This step starts with the bonding parameter request packet. This packet sets necessary bond parameters such as the client MAC address and to gain the required information to synchronize the client and peripheral.
# | Tag | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | 0x01 | Status | 0 | |
2 | 0x03 | Client serial | string | >= 1 |
3 | 0x04 | Protocol version | byte | 1 |
4 | 0x05 | Maximum frame size | 0 | |
5 | 0x07 | Client MAC address | string | 17 |
6 | 0x09 | Encrypted message count | uint32 | 4 |
An example of the bonding parameter request packet: 5a 00 27 00 01 0f 01 00 03 06 4d 7a 41 30 4e 30 04 01 02 05 00 07 11 46 46 3a 46 46 3a 46 46 3a 46 46 3a 46 46 3a 43 43 09 00 4b c8
The client serial is the hardware serial number and is usually retrieved by using the Build.getSerial()
method from the Android SDK.
In case the hardware serial number is not available, the first 6 characters of the MAC address will also suffice.
# | Tag | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | 0x01 | Status | VarInt | >= 1 |
2 | 0x03 | Client serial | string | 6 |
3 | 0x04 | Protocol version | byte | 1 |
4 | 0x05 | Maximum frame size | VarInt | >= 1 |
5 | 0x07 | Client MAC address | string | 17 |
6 | 0x08 | Device MAC address | string | 17 |
7 | 0x09 | Encrypted message count | uint32 | 4 |
An example of the bonding parameter response packet: 5a 00 3c 00 01 0f 01 01 00 02 01 03 04 01 02 05 02 00 f4 07 11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 09 04 00 00 00 01 bb 56
The peripheral responds with the necessary parameters to initiate bonding the peripheral to the client device. For an unknown reason, the client and device MAC addresses are empty in the response.
Bonding/pairing
In case the client is not already associated with the peripheral, the client can optionally send a bonding request packet. This request causes a prompt to show on the peripheral where the user can confirm the bonding request. If the bonding request is confirmed, the peripheral will attempt to setup a permanent connection with the client.
Cryptographic encryption
An initialization vector (IV) or starting variable (SV) is an input to a cryptographic algorithm being used to provide the initial state. The IV is typically required to be random or pseudorandom, but sometimes an IV only needs to be unpredictable or unique.
In case of the Huawei Link protocol, the IV is computed by taking 12 random bytes and concatenating 4 bytes of the encryption counter integer from the bonding parameters, see bonding parameter negotiation. The counter must be appended to the array in the big-endian order. The process is shown in Kotlin in the code snippet below:
fun computeInitializationVector(counter: Int): ByteArray {
val random = SecureRandom()
val bytes = ByteArray(12)
random.nextBytes(bytes)
val buffer = ByteBuffer.allocate(4)
buffer.order(ByteOrder.BIG_ENDIAN)
buffer.putInt(counter)
return bytes + buffer.array()
}
After computing a 16 byte initialization vector, the AES encryption function can be called. To read more about the encryption process, see Wikipedia.
The Huawei Link protocol encryption stack consists the AES encryption technique, with the CBC operation mode and PCKS7 padding. The code snippet below shows the encryption and decryption functions in Kotlin:
private val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
fun encrypt(bytes: ByteArray, key: ByteArray, iv: ByteArray): ByteArray {
val secretKeySpec = SecretKeySpec(key, "AES")
val paramSpec: AlgorithmParameterSpec = IvParameterSpec(iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, paramSpec)
return cipher.doFinal(bytes)
}
fun decrypt(bytes: ByteArray, key: ByteArray, iv: ByteArray): ByteArray {
val secretKeySpec = SecretKeySpec(key, "AES")
val paramSpec: AlgorithmParameterSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, paramSpec)
return cipher.doFinal(bytes)
}
Bonding key
In addition to the initialization vector from the previous chapter, a bonding key must be computed for the bonding request. The first step of computing the bonding key is generating a secret MAC address key. This is done by, among other things, executing bitwise operations on the given MAC address including a pre-defined secret key. The result of the bitwise operations is put into a byte array of which an SHA-256 hash is created. The first 16 bytes are taken from that array and used as the secret MAC address key. A code snippet describing this process in Kotlin is shown below:
private val sha256 = MessageDigest.getInstance("SHA-256")
@ExperimentalUnsignedTypes
private val SECRET_KEY = ubyteArrayOf(139u, 97u, 254u, 153u, 54u, 32u, 183u, 254u, 248u, 147u, 111u, 100u, 249u, 176u, 192u, 45u, 250u, 18u, 77u, 252u, 176u, 151u, 25u, 190u, 230u, 9u, 245u, 214u, 39u, 98u, 4u, 75u)
@ExperimentalUnsignedTypes
fun createSecretKey(macAddress: String): ByteArray {
val macAddressBytes = (macAddress.replace(":", "") + "0000").toByteArray().toUByteArray()
val finalMixedKey = UByteArray(macAddressBytes.size)
for (i in finalMixedKey.indices) {
finalMixedKey[i] = ((SECRET_KEY[i] shr 6) xor macAddressBytes[i]) and 0xFF.toUByte()
}
return sha256.digest(finalMixedKey.toByteArray()).take(16).toByteArray()
}
The second and final step of computing the bonding key is encrypting the secret MAC address key with the AES encryption technique as shown in the previously mentioned cryptographic encryption section. To create the bonding key, call the encrypt function from before with any arbitrary key as the value and use the computed secret MAC address key as the encryption key.
fun createBondingKey(macAddress: String, key: ByteArray, iv: ByteArray): ByteArray {
return encrypt(key, createSecretKey(macAddress), iv)
}
# | Tag | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | 0x01 | Bond request | 0 | |
2 | 0x03 | Request code | 0x00 | 1 |
3 | 0x05 | Client serial | string | 6 |
4 | 0x06 | Bonding key | byte[] | 32 |
5 | 0x07 | Initialization vector | byte[] | 16 |
6 | 0x09 | Client name | string | >= 1 |
An optional client name can be passed to the packet, which is shown on the peripheral bonding confirmation prompt.
An example of the bonding request packet: 5a 00 53 00 01 0e 01 00 03 01 00 05 06 4d 7a 41 30 4e 30 06 20 68 40 0f c7 d3 8a 93 9b 24 a3 1f f3 e5 33 86 53 28 87 31 69 f3 79 ab 04 23 56 61 96 32 72 9a cd 07 10 59 e7 6b ed 88 55 23 00 41 30 b5 7b 4f b0 4a 72 09 0d 4d 69 20 31 30 20 4c 69 74 65 20 35 47 36 54
After the bond request is received on the peripheral, the peripheral returns the bond status indicating whether the bonding was successful.
# | Tag | Field | Value type | Length (bytes) |
---|---|---|---|---|
1 | 0x02 | Status | VarInt | >= 1 |
An example of the bonding request response: 5a 00 06 00 01 0e 02 01 00 17 c0