swiftencryptionaescryptoswiftapple-cryptokit

Swift decrypt AES CCM message


I'm trying to decrypt a message in Swift that is sent from a device which I've connected via Bluetooth.

I'm trying to make a swift implementation of this article:

https://www.makinolo.com/blog/2023/10/08/connecting-to-zwift-play-controllers/

I've created a symmetric key through the following steps:

let privateKey = P256.KeyAgreement.PrivateKey()
var message = "RideOn".data(using: .utf8)!
message.append(Data([0x01, 0x09]))
let publicKey = privateKey.publicKey
message.append(publicKey.rawRepresentation)
/// sent to peripheral

I receive their public key and create the shared secret

        let rideOn = "RideOn".data(using: .utf8)!

        guard value.starts(with: rideOn) else {
            logger.error("no ride on?")
            return
        }

        let key = value.dropFirst(8)

        guard let publicKey = try? P256.KeyAgreement.PublicKey(rawRepresentation: key) else {
            logger.debug("wrong key?")
            return
        }

        let salt = key + privateKey.publicKey.rawRepresentation

        guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else {
            logger.debug("no shared secret")
            return
        }
        let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data(), outputByteCount: 36)

I start receiving messages, but I cannot get the messages to decode. These steps do not seem to work for iOS, because of a difference nonce maybe?

For decryption you need to extract the 3 parts of the message and call the AES-CCM decrypt function with:

- The AES mode, which is aes-256-ccm
- The IV (initialization Vector) also called nonce (number used once) which is an 8 byte array whose 4 first bytes are the 4 last bytes of the HKDF symmetric key, followed by the 4 bytes of the counter you have received in the message.
The IV / nonce length, which is 8 bytes
- The received MIC, so the algorithm can compare his own calculated MIC with the one provided and authenticate the message
- The received message (only the remaining bytes in the message after stripping the counter and the MIC)

I've tried the following:

Using cryptoswift:

        let nonceData = symmetricKey.withUnsafeBytes { Data(Array($0)) }[...3] + value[...3]
        let tag = value.suffix(4)
        var value = value
        value = value.dropFirst(4)
        value.removeLast(4)

        do {
            let aes = try AES(key: symmetricKey.withUnsafeBytes { Array($0) }, blockMode: CCM(iv: [UInt8](nonceData), tagLength: 4, messageLength: value.count), padding: .zeroPadding)
            let result = try aes.decrypt([UInt8](value))

            print("Result: \(result)")
        } catch {
            logger.debug("Error: \(error.localizedDescription)")
        }

using CryptoKit

        do {
            let nonce = try AES.GCM.Nonce(data: nonceData)

            let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: value, tag: tag)
            let result = try AES.GCM.open(sealedBox, using: symmetricKey)

        } catch {
            logger.debug("\(error.localizedDescription)")
        }

I think there is a problem with creating the Nonce, but no real clue, not really familiar with encryption/decryption.

This is a couple of the messages I receive:

Own private key: [36, 147, 90, 234, 66, 250, 58, 153, 187, 163, 237, 184, 100, 91, 99, 35, 139, 69, 181, 239, 250, 214, 164, 116, 118, 58, 95, 218, 28, 25, 111, 105]
Own public key: [34, 76, 142, 17, 129, 101, 252, 53, 92, 200, 144, 34, 20, 43, 115, 100, 181, 27, 19, 176, 145, 253, 34, 241, 74, 107, 125, 254, 130, 146, 169, 65, 118, 6, 199, 146, 89, 20, 47, 233, 101, 134, 76, 192, 61, 167, 135, 89, 100, 140, 56, 63, 3, 117, 57, 52, 136, 220, 38, 44, 119, 8, 110, 201]
Click key: [247, 134, 157, 189, 154, 129, 53, 47, 254, 165, 164, 49, 150, 55, 151, 75, 7, 182, 85, 194, 28, 198, 177, 153, 128, 75, 32, 50, 155, 204, 225, 201, 215, 241, 81, 246, 9, 222, 89, 81, 11, 190, 74, 214, 117, 41, 155, 227, 87, 41, 120, 34, 141, 227, 83, 85, 22, 246, 115, 0, 65, 174, 194, 48]
Salt: [247, 134, 157, 189, 154, 129, 53, 47, 254, 165, 164, 49, 150, 55, 151, 75, 7, 182, 85, 194, 28, 198, 177, 153, 128, 75, 32, 50, 155, 204, 225, 201, 215, 241, 81, 246, 9, 222, 89, 81, 11, 190, 74, 214, 117, 41, 155, 227, 87, 41, 120, 34, 141, 227, 83, 85, 22, 246, 115, 0, 65, 174, 194, 48]

After pressing click, a couple of the messages:
[21, 0, 0, 0, 248, 68, 228, 177, 135, 249, 158, 236, 165]
[22, 0, 0, 0, 78, 244, 116, 18, 109, 111, 238, 175, 86]
[23, 0, 0, 0, 122, 90, 33, 237, 14, 237, 234]
[24, 0, 0, 0, 139, 66, 8, 195, 217, 18, 234]
[25, 0, 0, 0, 3, 247, 14, 46, 58, 88, 71]


Solution

  • Please check the following points in your code:

    With these changes decryption works for me (as the successful authentication proves).

    However, I only verified the shared secret and the HKDF key with the posted Swift code (modified as described above), but not the CCM decryption itself (as I currently do not have an environment with CryptoSwift available).
    I have successfully performed the AES/CCM decryption in Python with the parameters determined in the (modified) Swift code, which proves that the ciphertext can in principle be successfully decrypted with these parameters using AES/CCM. So if the CCM implementation of CryptoSwift is not buggy, the decryption of the given ciphertext with CryptoSwift should also be successful.

    Regarding your CryptoKit decryption: This cannot work because GCM and CCM are different operating modes that are not compatible.


    Here are the intermediate values that were generated with the (modified) Swift code (apart from the last value decrypted, which was determined with Python as mentioned). This should at least make it easier for you to narrow down possible bugs (all values are hex encoded):

    other public key, hex: f7869dbd9a81352ffea5a4319637974b07b655c21cc6b199804b20329bcce1c9d7f151f609de59510bbe4ad675299be3572978228de3535516f6730041aec230
    private key, hex:      24935aea42fa3a99bba3edb8645b63238b45b5effad6a474763a5fda1c196f69
    public key, hex:       224c8e118165fc355cc89022142b7364b51b13b091fd22f14a6b7dfe8292a9417606c79259142fe965864cc03da78759648c383f0375393488dc262c77086ec9
    salt, hex:             f7869dbd9a81352ffea5a4319637974b07b655c21cc6b199804b20329bcce1c9d7f151f609de59510bbe4ad675299be3572978228de3535516f6730041aec230224c8e118165fc355cc89022142b7364b51b13b091fd22f14a6b7dfe8292a9417606c79259142fe965864cc03da78759648c383f0375393488dc262c77086ec9
    shared secret, hex:    a9137e57dd92892464a807b19fa353d5dd39a1b5a1153ae36fb7a86ee75248e5
    HKDF key, hex:         211a0ef8a89e5c60962b9c9dd9b7492328c0115a0db840e710b9cace78668cdf8cb25c08
    

    For the first message: [21, 0, 0, 0, 248, 68, 228, 177, 135, 249, 158, 236, 165]:

    message, hex:          15000000f844e4b187f99eeca5
    counter, hex:          15000000
    ciphertext, hex:       f844e4b187
    tag, hex:              f99eeca5
    ciphertext|tag, hex:   f844e4b187f99eeca5
    nonce, hex:            8cb25c0815000000
    AES key, hex:          211a0ef8a89e5c60962b9c9dd9b7492328c0115a0db840e710b9cace78668cdf
    decrypted, hex:        3708011001