@bota-dev/react-native-sdk 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -2
- package/lib/commonjs/ble/constants.js +28 -2
- package/lib/commonjs/ble/constants.js.map +1 -1
- package/lib/commonjs/ble/parsers.js +128 -0
- package/lib/commonjs/ble/parsers.js.map +1 -1
- package/lib/commonjs/managers/DeviceManager.js +179 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -1
- package/lib/commonjs/utils/crypto.js +169 -0
- package/lib/commonjs/utils/crypto.js.map +1 -0
- package/lib/commonjs/utils/index.js +12 -0
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/ble/constants.js +26 -0
- package/lib/module/ble/constants.js.map +1 -1
- package/lib/module/ble/parsers.js +124 -1
- package/lib/module/ble/parsers.js.map +1 -1
- package/lib/module/managers/DeviceManager.js +181 -2
- package/lib/module/managers/DeviceManager.js.map +1 -1
- package/lib/module/utils/crypto.js +160 -0
- package/lib/module/utils/crypto.js.map +1 -0
- package/lib/module/utils/index.js +1 -0
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/src/ble/constants.d.ts +18 -0
- package/lib/typescript/src/ble/constants.d.ts.map +1 -1
- package/lib/typescript/src/ble/parsers.d.ts +34 -1
- package/lib/typescript/src/ble/parsers.d.ts.map +1 -1
- package/lib/typescript/src/managers/DeviceManager.d.ts +73 -1
- package/lib/typescript/src/managers/DeviceManager.d.ts.map +1 -1
- package/lib/typescript/src/models/Device.d.ts +65 -0
- package/lib/typescript/src/models/Device.d.ts.map +1 -1
- package/lib/typescript/src/utils/crypto.d.ts +83 -0
- package/lib/typescript/src/utils/crypto.d.ts.map +1 -0
- package/lib/typescript/src/utils/index.d.ts +1 -0
- package/lib/typescript/src/utils/index.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/ble/constants.ts +26 -0
- package/src/ble/parsers.ts +131 -0
- package/src/managers/DeviceManager.ts +244 -0
- package/src/models/Device.ts +76 -0
- package/src/utils/crypto.ts +221 -0
- package/src/utils/index.ts +1 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
SERVICE_DEVICE_INFO,
|
|
12
12
|
SERVICE_BOTA_PROVISIONING,
|
|
13
13
|
SERVICE_BOTA_CONTROL,
|
|
14
|
+
SERVICE_BOTA_WIFI_CONFIG,
|
|
14
15
|
CHAR_SERIAL_NUMBER,
|
|
15
16
|
CHAR_FIRMWARE_REVISION,
|
|
16
17
|
CHAR_HARDWARE_REVISION,
|
|
@@ -22,6 +23,9 @@ import {
|
|
|
22
23
|
CHAR_RECORDING_CONTROL,
|
|
23
24
|
CHAR_RECORDING_STATUS,
|
|
24
25
|
CHAR_TIME_SYNC,
|
|
26
|
+
CHAR_WIFI_GRANT,
|
|
27
|
+
CHAR_WIFI_CREDENTIAL,
|
|
28
|
+
CHAR_WIFI_STATUS,
|
|
25
29
|
API_ENDPOINT_PRODUCTION,
|
|
26
30
|
API_ENDPOINT_SANDBOX,
|
|
27
31
|
PROVISIONING_SUCCESS,
|
|
@@ -41,6 +45,9 @@ import {
|
|
|
41
45
|
parsePairingState,
|
|
42
46
|
parseDeviceStatus,
|
|
43
47
|
createTimeSyncData,
|
|
48
|
+
parseWiFiStatusInfo,
|
|
49
|
+
parseWiFiConfigResult,
|
|
50
|
+
createWiFiGrantPacket,
|
|
44
51
|
} from '../ble/parsers';
|
|
45
52
|
import type {
|
|
46
53
|
DiscoveredDevice,
|
|
@@ -53,6 +60,10 @@ import type {
|
|
|
53
60
|
// RecordingCommand, // TODO: Re-enable when used
|
|
54
61
|
StartRecordingOptions,
|
|
55
62
|
StopRecordingOptions,
|
|
63
|
+
WiFiConfigGrant,
|
|
64
|
+
WiFiCredentials,
|
|
65
|
+
WiFiConfigResult,
|
|
66
|
+
WiFiStatusInfo,
|
|
56
67
|
} from '../models/Device';
|
|
57
68
|
import type { DeviceManagerEvents } from '../models/Status';
|
|
58
69
|
import {
|
|
@@ -60,6 +71,11 @@ import {
|
|
|
60
71
|
ProvisioningError,
|
|
61
72
|
} from '../utils/errors';
|
|
62
73
|
import { logger } from '../utils/logger';
|
|
74
|
+
import {
|
|
75
|
+
deriveSessionKey,
|
|
76
|
+
encryptWiFiCredentials,
|
|
77
|
+
formatWiFiCredentialPacket,
|
|
78
|
+
} from '../utils/crypto';
|
|
63
79
|
|
|
64
80
|
const log = logger.tag('DeviceManager');
|
|
65
81
|
|
|
@@ -776,6 +792,234 @@ export class DeviceManager extends EventEmitter<DeviceManagerEvents> {
|
|
|
776
792
|
};
|
|
777
793
|
}
|
|
778
794
|
|
|
795
|
+
// ============================================================================
|
|
796
|
+
// WiFi Upload Configuration
|
|
797
|
+
// ============================================================================
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Configure WiFi credentials on device via BLE.
|
|
801
|
+
* Requires a WiFi configuration grant from backend.
|
|
802
|
+
*
|
|
803
|
+
* @param deviceId - Connected device ID
|
|
804
|
+
* @param credentials - WiFi network credentials
|
|
805
|
+
* @param grant - WiFi config grant from backend
|
|
806
|
+
* @returns Configuration result
|
|
807
|
+
*
|
|
808
|
+
* @example
|
|
809
|
+
* ```typescript
|
|
810
|
+
* // 1. Get grant from backend
|
|
811
|
+
* const grant = await api.createWiFiConfigGrant(deviceId);
|
|
812
|
+
*
|
|
813
|
+
* // 2. Configure device via BLE
|
|
814
|
+
* const result = await deviceManager.configureWiFi(
|
|
815
|
+
* deviceId,
|
|
816
|
+
* { ssid: 'MyNetwork', password: 'password123', securityType: 'WPA2' },
|
|
817
|
+
* grant
|
|
818
|
+
* );
|
|
819
|
+
*
|
|
820
|
+
* if (result.success) {
|
|
821
|
+
* console.log('WiFi configured successfully');
|
|
822
|
+
* }
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
async configureWiFi(
|
|
826
|
+
deviceId: string,
|
|
827
|
+
credentials: WiFiCredentials,
|
|
828
|
+
grant: WiFiConfigGrant
|
|
829
|
+
): Promise<WiFiConfigResult> {
|
|
830
|
+
log.info('Configuring WiFi on device', { deviceId, ssid: credentials.ssid });
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
// Step 1: Submit WiFi config grant to device
|
|
834
|
+
const grantPacket = createWiFiGrantPacket(grant.grantBlob);
|
|
835
|
+
await this.bleManager.writeCharacteristic(
|
|
836
|
+
deviceId,
|
|
837
|
+
SERVICE_BOTA_WIFI_CONFIG,
|
|
838
|
+
CHAR_WIFI_GRANT,
|
|
839
|
+
grantPacket
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
log.debug('WiFi grant submitted');
|
|
843
|
+
|
|
844
|
+
// Step 2: Derive K_session from grant
|
|
845
|
+
const sessionKey = deriveSessionKey(grant.grantBlob);
|
|
846
|
+
|
|
847
|
+
// Step 3: Encrypt WiFi credentials with K_session
|
|
848
|
+
const encrypted = encryptWiFiCredentials(
|
|
849
|
+
credentials.ssid,
|
|
850
|
+
credentials.password,
|
|
851
|
+
sessionKey
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
// Step 4: Format credential packet for BLE transmission
|
|
855
|
+
const credentialPacket = formatWiFiCredentialPacket(encrypted);
|
|
856
|
+
|
|
857
|
+
log.debug('Sending encrypted WiFi credentials', {
|
|
858
|
+
packetSize: credentialPacket.length,
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// Step 5: Write encrypted credentials to device
|
|
862
|
+
await this.bleManager.writeCharacteristic(
|
|
863
|
+
deviceId,
|
|
864
|
+
SERVICE_BOTA_WIFI_CONFIG,
|
|
865
|
+
CHAR_WIFI_CREDENTIAL,
|
|
866
|
+
credentialPacket
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
// Step 6: Wait for configuration result
|
|
870
|
+
const result = await this.waitForWiFiConfigResult(deviceId);
|
|
871
|
+
|
|
872
|
+
if (result.success) {
|
|
873
|
+
log.info('WiFi configuration successful', { deviceId });
|
|
874
|
+
} else {
|
|
875
|
+
log.warn('WiFi configuration failed', { deviceId, error: result.error });
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return result;
|
|
879
|
+
} catch (error) {
|
|
880
|
+
log.error('WiFi configuration error', error instanceof Error ? error : undefined);
|
|
881
|
+
throw new DeviceError(
|
|
882
|
+
`Failed to configure WiFi: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
883
|
+
'WIFI_CONFIG_ERROR',
|
|
884
|
+
deviceId,
|
|
885
|
+
error instanceof Error ? error : undefined
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Get WiFi connection status from device.
|
|
892
|
+
*
|
|
893
|
+
* @param deviceId - Connected device ID
|
|
894
|
+
* @returns WiFi status information
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* ```typescript
|
|
898
|
+
* const status = await deviceManager.getWiFiStatus(deviceId);
|
|
899
|
+
* console.log('WiFi status:', status.status);
|
|
900
|
+
* if (status.status === 'connected') {
|
|
901
|
+
* console.log('Connected to:', status.ssid);
|
|
902
|
+
* console.log('Signal strength:', status.signalStrength);
|
|
903
|
+
* }
|
|
904
|
+
* ```
|
|
905
|
+
*/
|
|
906
|
+
async getWiFiStatus(deviceId: string): Promise<WiFiStatusInfo> {
|
|
907
|
+
log.debug('Reading WiFi status', { deviceId });
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
const data = await this.bleManager.readCharacteristic(
|
|
911
|
+
deviceId,
|
|
912
|
+
SERVICE_BOTA_WIFI_CONFIG,
|
|
913
|
+
CHAR_WIFI_STATUS
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
const status = parseWiFiStatusInfo(data);
|
|
917
|
+
|
|
918
|
+
log.debug('WiFi status', { deviceId, status: status.status, ssid: status.ssid });
|
|
919
|
+
|
|
920
|
+
return status;
|
|
921
|
+
} catch (error) {
|
|
922
|
+
log.error('Failed to read WiFi status', error instanceof Error ? error : undefined);
|
|
923
|
+
throw new DeviceError(
|
|
924
|
+
`Failed to read WiFi status: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
925
|
+
'WIFI_STATUS_ERROR',
|
|
926
|
+
deviceId,
|
|
927
|
+
error instanceof Error ? error : undefined
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Subscribe to WiFi status updates from device.
|
|
934
|
+
*
|
|
935
|
+
* @param deviceId - Connected device ID
|
|
936
|
+
* @param callback - Callback function for status updates
|
|
937
|
+
* @returns Subscription object (call .remove() to unsubscribe)
|
|
938
|
+
*
|
|
939
|
+
* @example
|
|
940
|
+
* ```typescript
|
|
941
|
+
* const subscription = deviceManager.subscribeToWiFiStatus(deviceId, (status) => {
|
|
942
|
+
* console.log('WiFi status update:', status.status);
|
|
943
|
+
* if (status.status === 'connected') {
|
|
944
|
+
* console.log('Connected to:', status.ssid);
|
|
945
|
+
* } else if (status.status === 'failed') {
|
|
946
|
+
* console.error('Connection failed:', status.lastError);
|
|
947
|
+
* }
|
|
948
|
+
* });
|
|
949
|
+
*
|
|
950
|
+
* // Later: unsubscribe
|
|
951
|
+
* subscription.remove();
|
|
952
|
+
* ```
|
|
953
|
+
*/
|
|
954
|
+
subscribeToWiFiStatus(
|
|
955
|
+
deviceId: string,
|
|
956
|
+
callback: (status: WiFiStatusInfo) => void
|
|
957
|
+
): Subscription {
|
|
958
|
+
log.debug('Subscribing to WiFi status', { deviceId });
|
|
959
|
+
|
|
960
|
+
return this.bleManager.subscribeToCharacteristic(
|
|
961
|
+
deviceId,
|
|
962
|
+
SERVICE_BOTA_WIFI_CONFIG,
|
|
963
|
+
CHAR_WIFI_STATUS,
|
|
964
|
+
(data) => {
|
|
965
|
+
try {
|
|
966
|
+
const status = parseWiFiStatusInfo(data);
|
|
967
|
+
callback(status);
|
|
968
|
+
} catch (error) {
|
|
969
|
+
log.error('Failed to parse WiFi status', error instanceof Error ? error : undefined);
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
(error) => {
|
|
973
|
+
log.error('WiFi status subscription error', error);
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Wait for WiFi configuration result from device.
|
|
980
|
+
*/
|
|
981
|
+
private waitForWiFiConfigResult(deviceId: string): Promise<WiFiConfigResult> {
|
|
982
|
+
return new Promise((resolve, reject) => {
|
|
983
|
+
const timeout = setTimeout(() => {
|
|
984
|
+
subscription.remove();
|
|
985
|
+
reject(new DeviceError(
|
|
986
|
+
'WiFi configuration timeout',
|
|
987
|
+
'WIFI_CONFIG_TIMEOUT',
|
|
988
|
+
deviceId
|
|
989
|
+
));
|
|
990
|
+
}, OPERATION_TIMEOUT);
|
|
991
|
+
|
|
992
|
+
const subscription = this.bleManager.subscribeToCharacteristic(
|
|
993
|
+
deviceId,
|
|
994
|
+
SERVICE_BOTA_WIFI_CONFIG,
|
|
995
|
+
CHAR_WIFI_STATUS,
|
|
996
|
+
(data) => {
|
|
997
|
+
try {
|
|
998
|
+
// Configuration result comes via status updates
|
|
999
|
+
const result = parseWiFiConfigResult(data);
|
|
1000
|
+
|
|
1001
|
+
clearTimeout(timeout);
|
|
1002
|
+
subscription.remove();
|
|
1003
|
+
resolve(result);
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
// Ignore parse errors, wait for valid result
|
|
1006
|
+
log.debug('Ignoring invalid WiFi config result', { error: error instanceof Error ? error.message : String(error) });
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
(error) => {
|
|
1010
|
+
clearTimeout(timeout);
|
|
1011
|
+
subscription.remove();
|
|
1012
|
+
reject(new DeviceError(
|
|
1013
|
+
`WiFi config result error: ${error.message}`,
|
|
1014
|
+
'WIFI_CONFIG_RESULT_ERROR',
|
|
1015
|
+
deviceId,
|
|
1016
|
+
error
|
|
1017
|
+
));
|
|
1018
|
+
}
|
|
1019
|
+
);
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
779
1023
|
/**
|
|
780
1024
|
* Clean up resources
|
|
781
1025
|
*/
|
package/src/models/Device.ts
CHANGED
|
@@ -76,6 +76,20 @@ export interface DiscoveredDevice {
|
|
|
76
76
|
discoveredAt: Date;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Device capabilities (bitmask)
|
|
81
|
+
*/
|
|
82
|
+
export interface DeviceCapabilities {
|
|
83
|
+
/** Supports Bluetooth Sync (BLE transfer) */
|
|
84
|
+
bleSync: boolean;
|
|
85
|
+
/** Supports WiFi Upload */
|
|
86
|
+
wifiUpload: boolean;
|
|
87
|
+
/** Supports Cellular Upload (4G/LTE) */
|
|
88
|
+
lteUpload: boolean;
|
|
89
|
+
/** Supports Remote Recording Control */
|
|
90
|
+
remoteRecord: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
/**
|
|
80
94
|
* Device after successful BLE connection
|
|
81
95
|
*/
|
|
@@ -96,6 +110,8 @@ export interface ConnectedDevice {
|
|
|
96
110
|
connectionState: ConnectionState;
|
|
97
111
|
/** Negotiated MTU size */
|
|
98
112
|
mtu: number;
|
|
113
|
+
/** Device capabilities */
|
|
114
|
+
capabilities?: DeviceCapabilities;
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
/**
|
|
@@ -255,3 +271,63 @@ export interface RecordingState {
|
|
|
255
271
|
/** Who initiated the recording */
|
|
256
272
|
initiatedBy?: 'local' | 'remote';
|
|
257
273
|
}
|
|
274
|
+
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// WiFi Upload Configuration Types
|
|
277
|
+
// ============================================================================
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* WiFi security type
|
|
281
|
+
*/
|
|
282
|
+
export type WiFiSecurityType = 'WPA2' | 'WPA3' | 'WEP' | 'OPEN';
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* WiFi connection status
|
|
286
|
+
*/
|
|
287
|
+
export type WiFiStatus = 'idle' | 'connecting' | 'connected' | 'failed' | 'disconnected';
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* WiFi configuration grant from backend
|
|
291
|
+
*/
|
|
292
|
+
export interface WiFiConfigGrant {
|
|
293
|
+
/** Grant blob (JWT token) */
|
|
294
|
+
grantBlob: string;
|
|
295
|
+
/** Expiration timestamp */
|
|
296
|
+
expiresAt: Date;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* WiFi credentials to configure
|
|
301
|
+
*/
|
|
302
|
+
export interface WiFiCredentials {
|
|
303
|
+
/** WiFi network SSID */
|
|
304
|
+
ssid: string;
|
|
305
|
+
/** WiFi password */
|
|
306
|
+
password: string;
|
|
307
|
+
/** Security type (default: WPA2) */
|
|
308
|
+
securityType?: WiFiSecurityType;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* WiFi configuration result from device
|
|
313
|
+
*/
|
|
314
|
+
export interface WiFiConfigResult {
|
|
315
|
+
/** Whether configuration was successful */
|
|
316
|
+
success: boolean;
|
|
317
|
+
/** Error code if failed */
|
|
318
|
+
error?: 'invalid_grant' | 'grant_expired' | 'decryption_error' | 'storage_error' | 'unknown';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* WiFi status information from device
|
|
323
|
+
*/
|
|
324
|
+
export interface WiFiStatusInfo {
|
|
325
|
+
/** Current WiFi connection status */
|
|
326
|
+
status: WiFiStatus;
|
|
327
|
+
/** Connected SSID (if connected) */
|
|
328
|
+
ssid?: string;
|
|
329
|
+
/** Signal strength (0-100) */
|
|
330
|
+
signalStrength?: number;
|
|
331
|
+
/** Last connection error (if failed) */
|
|
332
|
+
lastError?: string;
|
|
333
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic utilities for WiFi Upload configuration
|
|
3
|
+
*
|
|
4
|
+
* NOTE: This module requires react-native-quick-crypto to be installed.
|
|
5
|
+
* Add to your app: npm install react-native-quick-crypto
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Buffer } from 'buffer';
|
|
9
|
+
|
|
10
|
+
// Type-only import to avoid runtime errors if crypto is not installed
|
|
11
|
+
type CryptoModule = typeof import('crypto');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get crypto module (lazy loaded to avoid errors if not installed)
|
|
15
|
+
*/
|
|
16
|
+
function getCrypto(): CryptoModule {
|
|
17
|
+
try {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
19
|
+
return require('react-native-quick-crypto') as CryptoModule;
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'WiFi Upload requires react-native-quick-crypto. ' +
|
|
23
|
+
'Install it with: npm install react-native-quick-crypto'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Derive K_session from WiFi config grant.
|
|
30
|
+
* K_session is used by device and app to encrypt/decrypt WiFi credentials in transit.
|
|
31
|
+
*
|
|
32
|
+
* Derivation: K_session = HMAC-SHA256(grant_blob, "BOTA_WIFI_SESSION_V1")
|
|
33
|
+
*
|
|
34
|
+
* @param grantBlob - Base64-encoded JWT grant from backend
|
|
35
|
+
* @returns 32-byte K_session as hex string
|
|
36
|
+
*/
|
|
37
|
+
export function deriveSessionKey(grantBlob: string): string {
|
|
38
|
+
const crypto = getCrypto();
|
|
39
|
+
|
|
40
|
+
return crypto
|
|
41
|
+
.createHmac('sha256', grantBlob)
|
|
42
|
+
.update('BOTA_WIFI_SESSION_V1')
|
|
43
|
+
.digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Encrypt WiFi credentials for BLE transmission using ChaCha20-Poly1305.
|
|
48
|
+
*
|
|
49
|
+
* Format sent to device:
|
|
50
|
+
* - 12 bytes: nonce (random)
|
|
51
|
+
* - N bytes: encrypted SSID
|
|
52
|
+
* - 16 bytes: auth tag for SSID
|
|
53
|
+
* - M bytes: encrypted password
|
|
54
|
+
* - 16 bytes: auth tag for password
|
|
55
|
+
*
|
|
56
|
+
* @param ssid - WiFi network SSID
|
|
57
|
+
* @param password - WiFi password
|
|
58
|
+
* @param sessionKey - K_session derived from grant (hex string)
|
|
59
|
+
* @returns Encrypted payload ready for BLE transmission
|
|
60
|
+
*/
|
|
61
|
+
export function encryptWiFiCredentials(
|
|
62
|
+
ssid: string,
|
|
63
|
+
password: string,
|
|
64
|
+
sessionKey: string
|
|
65
|
+
): {
|
|
66
|
+
nonce: Buffer;
|
|
67
|
+
ssidEncrypted: Buffer;
|
|
68
|
+
ssidAuthTag: Buffer;
|
|
69
|
+
passwordEncrypted: Buffer;
|
|
70
|
+
passwordAuthTag: Buffer;
|
|
71
|
+
} {
|
|
72
|
+
const crypto = getCrypto();
|
|
73
|
+
|
|
74
|
+
// Convert session key from hex to Buffer
|
|
75
|
+
const keyBuffer = Buffer.from(sessionKey, 'hex');
|
|
76
|
+
|
|
77
|
+
// Generate random nonce (12 bytes for ChaCha20-Poly1305)
|
|
78
|
+
const nonce = crypto.randomBytes(12);
|
|
79
|
+
|
|
80
|
+
// Encrypt SSID
|
|
81
|
+
const ssidCipher = crypto.createCipheriv('chacha20-poly1305', keyBuffer, nonce, {
|
|
82
|
+
authTagLength: 16,
|
|
83
|
+
});
|
|
84
|
+
const ssidEncrypted = Buffer.concat([
|
|
85
|
+
ssidCipher.update(ssid, 'utf-8'),
|
|
86
|
+
ssidCipher.final(),
|
|
87
|
+
]);
|
|
88
|
+
const ssidAuthTag = ssidCipher.getAuthTag();
|
|
89
|
+
|
|
90
|
+
// Encrypt password (reuse same nonce with different additional data)
|
|
91
|
+
const passwordCipher = crypto.createCipheriv('chacha20-poly1305', keyBuffer, nonce, {
|
|
92
|
+
authTagLength: 16,
|
|
93
|
+
});
|
|
94
|
+
// Use different AAD to prevent nonce reuse issues
|
|
95
|
+
passwordCipher.setAAD(Buffer.from('password'), { plaintextLength: Buffer.byteLength(password, 'utf-8') });
|
|
96
|
+
const passwordEncrypted = Buffer.concat([
|
|
97
|
+
passwordCipher.update(password, 'utf-8'),
|
|
98
|
+
passwordCipher.final(),
|
|
99
|
+
]);
|
|
100
|
+
const passwordAuthTag = passwordCipher.getAuthTag();
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
nonce,
|
|
104
|
+
ssidEncrypted,
|
|
105
|
+
ssidAuthTag,
|
|
106
|
+
passwordEncrypted,
|
|
107
|
+
passwordAuthTag,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Decrypt WiFi credentials received from device (if needed for verification).
|
|
113
|
+
*
|
|
114
|
+
* @param encrypted - Encrypted SSID or password
|
|
115
|
+
* @param nonce - 12-byte nonce
|
|
116
|
+
* @param authTag - 16-byte authentication tag
|
|
117
|
+
* @param sessionKey - K_session derived from grant (hex string)
|
|
118
|
+
* @param aad - Additional authenticated data (optional, use 'password' for password field)
|
|
119
|
+
* @returns Decrypted plaintext
|
|
120
|
+
*/
|
|
121
|
+
export function decryptWiFiCredential(
|
|
122
|
+
encrypted: Buffer,
|
|
123
|
+
nonce: Buffer,
|
|
124
|
+
authTag: Buffer,
|
|
125
|
+
sessionKey: string,
|
|
126
|
+
aad?: string
|
|
127
|
+
): string {
|
|
128
|
+
const crypto = getCrypto();
|
|
129
|
+
|
|
130
|
+
// Convert session key from hex to Buffer
|
|
131
|
+
const keyBuffer = Buffer.from(sessionKey, 'hex');
|
|
132
|
+
|
|
133
|
+
// Decrypt with ChaCha20-Poly1305
|
|
134
|
+
const decipher = crypto.createDecipheriv('chacha20-poly1305', keyBuffer, nonce, {
|
|
135
|
+
authTagLength: 16,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
decipher.setAuthTag(authTag);
|
|
139
|
+
|
|
140
|
+
if (aad) {
|
|
141
|
+
decipher.setAAD(Buffer.from(aad), { plaintextLength: encrypted.length });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const decrypted = Buffer.concat([
|
|
145
|
+
decipher.update(encrypted),
|
|
146
|
+
decipher.final(),
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
return decrypted.toString('utf-8');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Format encrypted WiFi credentials for BLE transmission.
|
|
154
|
+
*
|
|
155
|
+
* Packet format:
|
|
156
|
+
* [nonce (12 bytes)][ssid_encrypted (N bytes)][ssid_tag (16 bytes)]
|
|
157
|
+
* [password_encrypted (M bytes)][password_tag (16 bytes)]
|
|
158
|
+
*
|
|
159
|
+
* @param encrypted - Result from encryptWiFiCredentials
|
|
160
|
+
* @returns Single buffer ready for BLE write
|
|
161
|
+
*/
|
|
162
|
+
export function formatWiFiCredentialPacket(encrypted: {
|
|
163
|
+
nonce: Buffer;
|
|
164
|
+
ssidEncrypted: Buffer;
|
|
165
|
+
ssidAuthTag: Buffer;
|
|
166
|
+
passwordEncrypted: Buffer;
|
|
167
|
+
passwordAuthTag: Buffer;
|
|
168
|
+
}): Buffer {
|
|
169
|
+
return Buffer.concat([
|
|
170
|
+
encrypted.nonce,
|
|
171
|
+
encrypted.ssidEncrypted,
|
|
172
|
+
encrypted.ssidAuthTag,
|
|
173
|
+
encrypted.passwordEncrypted,
|
|
174
|
+
encrypted.passwordAuthTag,
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parse encrypted WiFi credential packet received from device.
|
|
180
|
+
*
|
|
181
|
+
* @param packet - Buffer received from BLE
|
|
182
|
+
* @param ssidLength - Expected SSID encrypted length
|
|
183
|
+
* @param passwordLength - Expected password encrypted length
|
|
184
|
+
* @returns Parsed components
|
|
185
|
+
*/
|
|
186
|
+
export function parseWiFiCredentialPacket(
|
|
187
|
+
packet: Buffer,
|
|
188
|
+
ssidLength: number,
|
|
189
|
+
passwordLength: number
|
|
190
|
+
): {
|
|
191
|
+
nonce: Buffer;
|
|
192
|
+
ssidEncrypted: Buffer;
|
|
193
|
+
ssidAuthTag: Buffer;
|
|
194
|
+
passwordEncrypted: Buffer;
|
|
195
|
+
passwordAuthTag: Buffer;
|
|
196
|
+
} {
|
|
197
|
+
let offset = 0;
|
|
198
|
+
|
|
199
|
+
const nonce = packet.subarray(offset, offset + 12);
|
|
200
|
+
offset += 12;
|
|
201
|
+
|
|
202
|
+
const ssidEncrypted = packet.subarray(offset, offset + ssidLength);
|
|
203
|
+
offset += ssidLength;
|
|
204
|
+
|
|
205
|
+
const ssidAuthTag = packet.subarray(offset, offset + 16);
|
|
206
|
+
offset += 16;
|
|
207
|
+
|
|
208
|
+
const passwordEncrypted = packet.subarray(offset, offset + passwordLength);
|
|
209
|
+
offset += passwordLength;
|
|
210
|
+
|
|
211
|
+
const passwordAuthTag = packet.subarray(offset, offset + 16);
|
|
212
|
+
offset += 16;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
nonce,
|
|
216
|
+
ssidEncrypted,
|
|
217
|
+
ssidAuthTag,
|
|
218
|
+
passwordEncrypted,
|
|
219
|
+
passwordAuthTag,
|
|
220
|
+
};
|
|
221
|
+
}
|
package/src/utils/index.ts
CHANGED