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:

  1. Bluetooth link parameter negotiation
  2. Challenge-response authentication
  3. Bonding/pairing parameters negotiation
  4. (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