mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
cleanup
This commit is contained in:
@@ -1,19 +1,13 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/protocol/zwift.pb.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart';
|
||||
import 'package:swift_control/utils/crypto/local_key_provider.dart';
|
||||
import 'package:swift_control/utils/crypto/zap_crypto.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
import '../../messages/notification.dart';
|
||||
import 'constants.dart';
|
||||
|
||||
class ZwiftClick extends ZwiftDevice {
|
||||
ZwiftClick(super.scanResult) : super(availableButtons: [ZwiftButtons.shiftUpRight, ZwiftButtons.shiftDownLeft]);
|
||||
|
||||
final zapEncryption = ZapCrypto(LocalKeyProvider());
|
||||
|
||||
@override
|
||||
List<ControllerButton> processClickNotification(Uint8List message) {
|
||||
final status = ClickKeyPadStatus.fromBuffer(message);
|
||||
@@ -26,64 +20,4 @@ class ZwiftClick extends ZwiftDevice {
|
||||
|
||||
@override
|
||||
String get latestFirmwareVersion => '1.1.0';
|
||||
|
||||
@override
|
||||
Future<void> setupHandshake() async {
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic!.uuid,
|
||||
Uint8List.fromList([
|
||||
...ZwiftConstants.RIDE_ON,
|
||||
...ZwiftConstants.REQUEST_START,
|
||||
...zapEncryption.localKeyProvider.getPublicKeyBytes(),
|
||||
]),
|
||||
withoutResponse: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void processDevicePublicKeyResponse(Uint8List bytes) {
|
||||
final devicePublicKeyBytes = bytes.sublist(
|
||||
ZwiftConstants.RIDE_ON.length + ZwiftConstants.RESPONSE_START_CLICK.length,
|
||||
);
|
||||
if (kDebugMode) {
|
||||
print("Device Public Key - ${devicePublicKeyBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}");
|
||||
}
|
||||
zapEncryption.initialise(devicePublicKeyBytes);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> processData(Uint8List bytes) async {
|
||||
int type;
|
||||
Uint8List message;
|
||||
|
||||
print('Processing encrypted data: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}');
|
||||
|
||||
final counter = bytes.sublist(0, 4); // Int.SIZE_BYTES is 4
|
||||
final payload = bytes.sublist(4);
|
||||
|
||||
print(
|
||||
'Counter: ${counter.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}',
|
||||
);
|
||||
print(
|
||||
'Payload: ${payload.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}',
|
||||
);
|
||||
|
||||
if (zapEncryption.encryptionKeyBytes == null) {
|
||||
actionStreamInternal.add(
|
||||
LogNotification(
|
||||
'Encryption not initialized, yet. You may need to update the firmware of your device with the Zwift Companion app.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final data = zapEncryption.decrypt(counter, payload);
|
||||
type = data[0];
|
||||
message = data.sublist(1);
|
||||
|
||||
print(
|
||||
'Decrypted Data: ${data.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pointycastle/digests/sha256.dart';
|
||||
import 'package:pointycastle/ecc/api.dart';
|
||||
import 'package:pointycastle/key_derivators/api.dart';
|
||||
import 'package:pointycastle/key_derivators/hkdf.dart';
|
||||
|
||||
class EncryptionUtils {
|
||||
static const int KEY_LENGTH = 32;
|
||||
static const int HKDF_LENGTH = 36;
|
||||
static const int MAC_LENGTH = 4;
|
||||
|
||||
static Uint8List publicKeyToByteArray(ECPublicKey ecPublicKey) {
|
||||
final affineX = ecPublicKey.Q!.x!.toBigInteger();
|
||||
final affineY = ecPublicKey.Q!.y!.toBigInteger();
|
||||
final affineXUnsigned = _bigIntToUnsignedBytes(affineX!);
|
||||
final affineYUnsigned = _bigIntToUnsignedBytes(affineY!);
|
||||
return Uint8List.fromList([...affineXUnsigned, ...affineYUnsigned]);
|
||||
}
|
||||
|
||||
static ECPublicKey generatePublicKey(Uint8List publicKeyBytes, ECDomainParameters params) {
|
||||
final bitLength = params.n.bitLength ~/ 8;
|
||||
final xBytes = publicKeyBytes.sublist(0, bitLength);
|
||||
final yBytes = publicKeyBytes.sublist(bitLength, 2 * bitLength);
|
||||
final x = BigInt.parse(xBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(), radix: 16);
|
||||
final y = BigInt.parse(yBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(), radix: 16);
|
||||
final ecPoint = params.curve.createPoint(x, y);
|
||||
return ECPublicKey(ecPoint, params);
|
||||
}
|
||||
|
||||
static Uint8List generateSharedSecretBytes(ECPrivateKey privateKey, ECPublicKey serverPublicKey) {
|
||||
final ecdh = ECDHBasicAgreement();
|
||||
ecdh.init(privateKey);
|
||||
final sharedSecret = ecdh.calculateAgreement(serverPublicKey);
|
||||
return _bigIntToUnsignedBytes(sharedSecret);
|
||||
}
|
||||
|
||||
static Uint8List generateHKDFBytes(Uint8List secretKey, Uint8List salt) {
|
||||
final hkdf = HKDFKeyDerivator(SHA256Digest());
|
||||
final params = HkdfParameters(secretKey, HKDF_LENGTH, salt);
|
||||
hkdf.init(params);
|
||||
final result = Uint8List(HKDF_LENGTH);
|
||||
hkdf.deriveKey(null, 0, result, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
static Uint8List _bigIntToUnsignedBytes(BigInt number) {
|
||||
final bytes = number.toRadixString(16).padLeft((number.bitLength + 7) >> 3 << 1, '0');
|
||||
return Uint8List.fromList(
|
||||
List<int>.generate(bytes.length ~/ 2, (i) => int.parse(bytes.substring(i * 2, i * 2 + 2), radix: 16)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pointycastle/export.dart';
|
||||
import 'package:swift_control/utils/crypto/encryption_utils.dart';
|
||||
|
||||
class LocalKeyProvider {
|
||||
late AsymmetricKeyPair<PublicKey, PrivateKey> pair;
|
||||
|
||||
LocalKeyProvider() {
|
||||
generateKeyPair();
|
||||
}
|
||||
|
||||
Uint8List getPublicKeyBytes() {
|
||||
return EncryptionUtils.publicKeyToByteArray(pair.publicKey as ECPublicKey);
|
||||
}
|
||||
|
||||
ECPublicKey getPublicKey() {
|
||||
return pair.publicKey as ECPublicKey;
|
||||
}
|
||||
|
||||
ECPrivateKey getPrivateKey() {
|
||||
return pair.privateKey as ECPrivateKey;
|
||||
}
|
||||
|
||||
void generateKeyPair() {
|
||||
final keyParams = ECKeyGeneratorParameters(ECCurve_secp256r1());
|
||||
final secureRandom = FortunaRandom();
|
||||
final random = Random.secure();
|
||||
final seeds = List<int>.generate(32, (_) => random.nextInt(256));
|
||||
secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));
|
||||
|
||||
final keyGenerator = ECKeyGenerator();
|
||||
keyGenerator.init(ParametersWithRandom(keyParams, secureRandom));
|
||||
pair = keyGenerator.generateKeyPair();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pointycastle/export.dart';
|
||||
import 'package:swift_control/utils/crypto/encryption_utils.dart';
|
||||
|
||||
import 'local_key_provider.dart';
|
||||
|
||||
class ZapCrypto {
|
||||
final LocalKeyProvider localKeyProvider;
|
||||
final AESEngine aesEngine = AESEngine();
|
||||
|
||||
Uint8List? encryptionKeyBytes;
|
||||
Uint8List? ivBytes;
|
||||
int counter = 0;
|
||||
|
||||
ZapCrypto(this.localKeyProvider);
|
||||
|
||||
void initialise(Uint8List devicePublicKeyBytes) {
|
||||
final hkdfBytes = generateHmacKeyDerivationFunctionBytes(devicePublicKeyBytes);
|
||||
encryptionKeyBytes = hkdfBytes.sublist(0, EncryptionUtils.KEY_LENGTH);
|
||||
ivBytes = hkdfBytes.sublist(32, EncryptionUtils.HKDF_LENGTH);
|
||||
}
|
||||
|
||||
Uint8List encrypt(Uint8List data) {
|
||||
assert(encryptionKeyBytes != null && ivBytes != null, "Not initialised");
|
||||
|
||||
final counterValue = counter;
|
||||
counter++;
|
||||
|
||||
final nonceBytes = createNonce(ivBytes!, counterValue);
|
||||
final encrypted = encryptDecrypt(true, nonceBytes, data);
|
||||
return Uint8List.fromList(createCounterBytes(counterValue) + encrypted);
|
||||
}
|
||||
|
||||
Uint8List decrypt(Uint8List counterArray, Uint8List payload) {
|
||||
assert(encryptionKeyBytes != null && ivBytes != null, "Not initialised");
|
||||
|
||||
final nonceBytes = Uint8List.fromList(ivBytes! + counterArray);
|
||||
return encryptDecrypt(false, nonceBytes, payload);
|
||||
}
|
||||
|
||||
Uint8List encryptDecrypt(bool encrypt, Uint8List nonceBytes, Uint8List data) {
|
||||
final aeadParameters = AEADParameters(
|
||||
KeyParameter(encryptionKeyBytes!),
|
||||
EncryptionUtils.MAC_LENGTH * 8,
|
||||
nonceBytes,
|
||||
Uint8List(0),
|
||||
);
|
||||
final ccmBlockCipher = CCMBlockCipher(aesEngine);
|
||||
ccmBlockCipher.init(encrypt, aeadParameters);
|
||||
final processed = Uint8List(ccmBlockCipher.getOutputSize(data.length));
|
||||
ccmBlockCipher.processBytes(data, 0, data.length, processed, 0);
|
||||
ccmBlockCipher.doFinal(processed, 0);
|
||||
return processed;
|
||||
}
|
||||
|
||||
Uint8List generateHmacKeyDerivationFunctionBytes(Uint8List devicePublicKeyBytes) {
|
||||
final serverPublicKey = EncryptionUtils.generatePublicKey(
|
||||
devicePublicKeyBytes,
|
||||
localKeyProvider.getPublicKey().parameters!,
|
||||
);
|
||||
final sharedSecretBytes = EncryptionUtils.generateSharedSecretBytes(
|
||||
localKeyProvider.getPrivateKey(),
|
||||
serverPublicKey,
|
||||
);
|
||||
final salt = Uint8List.fromList(
|
||||
EncryptionUtils.publicKeyToByteArray(serverPublicKey) + localKeyProvider.getPublicKeyBytes(),
|
||||
);
|
||||
return EncryptionUtils.generateHKDFBytes(sharedSecretBytes, salt);
|
||||
}
|
||||
|
||||
Uint8List createNonce(Uint8List iv, int messageCounter) {
|
||||
return Uint8List.fromList(iv + createCounterBytes(messageCounter));
|
||||
}
|
||||
|
||||
Uint8List createCounterBytes(int messageCounter) {
|
||||
final buffer = ByteData(4);
|
||||
buffer.setInt32(0, messageCounter, Endian.little);
|
||||
return buffer.buffer.asUint8List();
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pb.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/protocol/zwift.pb.dart' hide RideButtonMask;
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/remote.dart';
|
||||
import 'package:swift_control/utils/crypto/local_key_provider.dart';
|
||||
import 'package:swift_control/utils/crypto/zap_crypto.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/my_whoosh.dart';
|
||||
import 'package:swift_control/utils/requirements/multi.dart';
|
||||
import 'package:swift_control/utils/requirements/platform.dart';
|
||||
@@ -26,7 +24,6 @@ bool _isAdvertising = false;
|
||||
bool _isLoading = false;
|
||||
bool _isServiceAdded = false;
|
||||
bool _isSubscribedToEvents = false;
|
||||
final _zapEncryption = ZapCrypto(LocalKeyProvider());
|
||||
Central? _central;
|
||||
GATTCharacteristic? _asyncCharacteristic;
|
||||
|
||||
@@ -183,22 +180,6 @@ class ZwiftRequirement extends PlatformRequirement {
|
||||
syncTxCharacteristic,
|
||||
value: ZwiftConstants.RIDE_ON,
|
||||
);
|
||||
} else if (value.startsWith(handshake)) {
|
||||
final devicePublicKeyBytes = value.sublist(
|
||||
ZwiftConstants.RIDE_ON.length + ZwiftConstants.RESPONSE_START_CLICK_V2.length,
|
||||
);
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
"Device Public Key - ${devicePublicKeyBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}",
|
||||
);
|
||||
}
|
||||
_zapEncryption.initialise(devicePublicKeyBytes);
|
||||
// respond with our public key
|
||||
await peripheralManager.notifyCharacteristic(
|
||||
_central!,
|
||||
syncTxCharacteristic,
|
||||
value: Uint8List.fromList(ZwiftConstants.RIDE_ON),
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -316,7 +297,7 @@ class ZwiftRequirement extends PlatformRequirement {
|
||||
manufacturerSpecificData: [
|
||||
ManufacturerSpecificData(
|
||||
id: 0x094A,
|
||||
data: Uint8List.fromList([ZwiftConstants.CLICK_V2_LEFT_SIDE, 0x43, 0x63]),
|
||||
data: Uint8List.fromList([ZwiftConstants.CLICK_V2_LEFT_SIDE, 0x13, 0x37]),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -342,7 +323,7 @@ class ZwiftRequirement extends PlatformRequirement {
|
||||
void writeCommand() {
|
||||
final status = RideKeyPadStatus()
|
||||
//..buttonMap = (~RideButtonMask.SHFT_UP_R_BTN.mask) & 0xFFFFFFFF
|
||||
//..buttonMap = (~RideButtonMask.SHFT_UP_L_BTN.mask) & 0xFFFFFFFF
|
||||
..buttonMap = (~RideButtonMask.SHFT_UP_L_BTN.mask) & 0xFFFFFFFF
|
||||
..buttonMap = (~RideButtonMask.LEFT_BTN.mask) & 0xFFFFFFFF
|
||||
..analogPaddles.clear();
|
||||
|
||||
@@ -362,11 +343,6 @@ class ZwiftRequirement extends PlatformRequirement {
|
||||
final zero = Uint8List.fromList([0x23, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
|
||||
peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: zero);
|
||||
}
|
||||
|
||||
int encodeActiveLowMap(RideButtonMask button, PlayButtonStatus status) {
|
||||
// 32-bit mask: pressed bit = 0, others = 1
|
||||
return (0xFFFFFFFF ^ button.mask) & 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
class _PairWidget extends StatefulWidget {
|
||||
|
||||
16
pubspec.lock
16
pubspec.lock
@@ -136,14 +136,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -804,14 +796,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -19,7 +19,6 @@ dependencies:
|
||||
url: https://github.com/jonasbark/universal_ble.git
|
||||
gamepads: ^0.1.8+2
|
||||
|
||||
pointycastle: any
|
||||
path_provider: ^2.1.5
|
||||
intl: any
|
||||
version: ^3.0.0
|
||||
|
||||
Reference in New Issue
Block a user