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]
Please check the following points in your code:
Although the salt seems to be correctly determined in the code with
let salt = key + privateKey.publicKey.rawRepresentation
in your sample data it only appears to consist of the public key of the device side.
The nonce seems to be determined incorrectly. According to the instructions, the nonce consists of the last 4 bytes of the HKDF key and the first 4 bytes of the message, i.e. it should be something like:
let nonceData = symmetricKey.withUnsafeBytes { Data(Array($0)) }.suffix(4) + valueMsg.prefix(4)
Also the AES key seems to be determined incorrectly. The AES key consists of the first 32 bytes (corresponds to AES-256) of the 36 byte HKDF key, i.e. it should be something like:
let aesKey = symmetricKey.withUnsafeBytes { Data(Array($0)) }.prefix(32)
In the AES/CCM decryption with CryptoSwift, the tag does not seem to be taken into account. The correct way would be to pass the concatenation valueCtTag
of ciphertext and tag to decrypt()
, where valueCtTag
is something like:
let valueCtTag = valueMsg.dropFirst(4)
As messageLength
specify: valueCtTag.count - 4
, as tagLength
apply: 4
.
Regarding the padding: It should not be .zeroPadding
, but .noPadding
.
See also the AES/CCM sample for tag length 4 from the CryptoSwift tests.
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