esp-idf-provisioning-web 0.0.1
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/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/ESPDevice.d.ts +66 -0
- package/dist/ESPDevice.d.ts.map +1 -0
- package/dist/ESPProvisionManager.d.ts +34 -0
- package/dist/ESPProvisionManager.d.ts.map +1 -0
- package/dist/constants.d.ts +19 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.js +5085 -0
- package/dist/protocol/protocol.d.ts +4 -0
- package/dist/protocol/protocol.d.ts.map +1 -0
- package/dist/protocol/wifiConfig.d.ts +8 -0
- package/dist/protocol/wifiConfig.d.ts.map +1 -0
- package/dist/protocol/wifiScan.d.ts +8 -0
- package/dist/protocol/wifiScan.d.ts.map +1 -0
- package/dist/security/Security.d.ts +26 -0
- package/dist/security/Security.d.ts.map +1 -0
- package/dist/security/Security0.d.ts +13 -0
- package/dist/security/Security0.d.ts.map +1 -0
- package/dist/transport/BLETransport.d.ts +22 -0
- package/dist/transport/BLETransport.d.ts.map +1 -0
- package/dist/transport/SoftAPTransport.d.ts +27 -0
- package/dist/transport/SoftAPTransport.d.ts.map +1 -0
- package/dist/transport/Transport.d.ts +22 -0
- package/dist/transport/Transport.d.ts.map +1 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/crypto.d.ts +44 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/package.json +60 -0
- package/src/ESPDevice.ts +231 -0
- package/src/ESPProvisionManager.ts +78 -0
- package/src/constants.ts +29 -0
- package/src/generated/.gitkeep +0 -0
- package/src/generated/proto.d.ts +3418 -0
- package/src/generated/proto.js +10006 -0
- package/src/index.ts +31 -0
- package/src/proto/constants.proto +14 -0
- package/src/proto/sec0.proto +28 -0
- package/src/proto/sec1.proto +45 -0
- package/src/proto/sec2.proto +48 -0
- package/src/proto/session.proto +25 -0
- package/src/proto/wifi_config.proto +57 -0
- package/src/proto/wifi_constants.proto +32 -0
- package/src/proto/wifi_ctrl.proto +38 -0
- package/src/proto/wifi_scan.proto +63 -0
- package/src/protocol/protocol.ts +23 -0
- package/src/protocol/wifiConfig.ts +108 -0
- package/src/protocol/wifiScan.ts +91 -0
- package/src/security/Security.ts +29 -0
- package/src/security/Security0.ts +40 -0
- package/src/transport/BLETransport.ts +111 -0
- package/src/transport/SoftAPTransport.ts +89 -0
- package/src/transport/Transport.ts +24 -0
- package/src/types.ts +86 -0
- package/src/utils/crypto.ts +129 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Transport } from './Transport';
|
|
2
|
+
// import { BLE_SERVICE_UUID, BLE_PROVISIONING_UUID, BLE_SESSION_UUID } from '../constants';
|
|
3
|
+
import { bytesToString } from '../utils/crypto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BLE Transport implementation using Web Bluetooth API
|
|
7
|
+
*/
|
|
8
|
+
export class BLETransport extends Transport {
|
|
9
|
+
private device: BluetoothDevice;
|
|
10
|
+
private serviceUUID: string;
|
|
11
|
+
private nuLookup: Map<string, string>;
|
|
12
|
+
|
|
13
|
+
private server?: BluetoothRemoteGATTServer;
|
|
14
|
+
private service?: BluetoothRemoteGATTService;
|
|
15
|
+
private connected: boolean = false;
|
|
16
|
+
|
|
17
|
+
constructor(device: BluetoothDevice, serviceUUID: string, nuLookup: Map<string, string>) {
|
|
18
|
+
super();
|
|
19
|
+
this.device = device;
|
|
20
|
+
this.serviceUUID = serviceUUID;
|
|
21
|
+
this.nuLookup = nuLookup;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async connect(): Promise<void> {
|
|
25
|
+
if (this.isConnected()) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Connect to GATT server
|
|
31
|
+
this.server = await this.device.gatt?.connect();
|
|
32
|
+
if (!this.server) {
|
|
33
|
+
throw new Error('Failed to connect to GATT server');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get provisioning services
|
|
37
|
+
try {
|
|
38
|
+
const services = await this.server.getPrimaryServices(this.serviceUUID);
|
|
39
|
+
if (services.length > 1) {
|
|
40
|
+
const service = services.find((s) => s.uuid === this.serviceUUID);
|
|
41
|
+
if (!service) {
|
|
42
|
+
throw new Error(`Service with UUID ${this.serviceUUID} not found`);
|
|
43
|
+
}
|
|
44
|
+
this.service = service;
|
|
45
|
+
} else {
|
|
46
|
+
this.service = services[0];
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`Failed to get primary services: ${error}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const characteristics = await this.service.getCharacteristics();
|
|
53
|
+
for (const char of characteristics) {
|
|
54
|
+
const descriptors = await char.getDescriptors();
|
|
55
|
+
for (const desc of descriptors) {
|
|
56
|
+
if (desc.uuid.slice(4, 8) !== '2901') continue;
|
|
57
|
+
const rawName = await desc.readValue();
|
|
58
|
+
this.nuLookup.set(bytesToString(new Uint8Array(rawName.buffer)).toLowerCase(), char.uuid);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.debug('BLE discovered characteristics:', this.nuLookup);
|
|
63
|
+
this.connected = true;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
this.connected = false;
|
|
66
|
+
throw new Error(`BLE connection failed: ${error}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
disconnect(): void {
|
|
71
|
+
if (this.server?.connected) {
|
|
72
|
+
this.server.disconnect();
|
|
73
|
+
}
|
|
74
|
+
this.server = undefined;
|
|
75
|
+
this.service = undefined;
|
|
76
|
+
this.connected = false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async sendData(endpoint: string, data: Uint8Array): Promise<Uint8Array> {
|
|
80
|
+
if (!this.isConnected()) {
|
|
81
|
+
throw new Error('Not connected to device');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const charUUID = this.nuLookup.get(endpoint.toLowerCase());
|
|
85
|
+
if (!charUUID) {
|
|
86
|
+
throw new Error(`Characteristic UUID for endpoint ${endpoint} not found`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const characteristic = await this.service?.getCharacteristic(charUUID);
|
|
90
|
+
if (!characteristic) {
|
|
91
|
+
throw new Error(`Characteristic for UUID ${charUUID} not found`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// FIXME: Uint8Array have wrong generic type constraints in this case
|
|
95
|
+
const cleanData = new Uint8Array(data.buffer as ArrayBuffer, data.byteOffset, data.byteLength);
|
|
96
|
+
await characteristic.writeValueWithResponse(cleanData);
|
|
97
|
+
const response = await characteristic.readValue();
|
|
98
|
+
return new Uint8Array(response.buffer);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
isConnected(): boolean {
|
|
102
|
+
return this.connected && this.server?.connected === true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the underlying Bluetooth device
|
|
107
|
+
*/
|
|
108
|
+
getDevice(): BluetoothDevice {
|
|
109
|
+
return this.device;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Transport } from './Transport';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SoftAP Transport implementation using HTTP
|
|
5
|
+
* Note: Browser cannot automatically connect to WiFi networks.
|
|
6
|
+
* User must manually connect to the device's SoftAP before using this transport.
|
|
7
|
+
*/
|
|
8
|
+
export class SoftAPTransport extends Transport {
|
|
9
|
+
private baseUrl: string;
|
|
10
|
+
private connected: boolean = false;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param baseUrl - Base URL of the device (e.g., http://192.168.4.1)
|
|
14
|
+
*/
|
|
15
|
+
constructor(baseUrl: string) {
|
|
16
|
+
super();
|
|
17
|
+
this.baseUrl = baseUrl;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async connect(): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
// Verify connection by making a test request
|
|
23
|
+
const response = await fetch(`${this.baseUrl}/proto-ver`, {
|
|
24
|
+
method: 'GET',
|
|
25
|
+
mode: 'cors',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`HTTP ${response.status}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.connected = true;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
this.connected = false;
|
|
35
|
+
throw new Error(
|
|
36
|
+
`SoftAP connection failed. Please ensure you are connected to the device's WiFi network. Error: ${error}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
disconnect(): void {
|
|
42
|
+
this.connected = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async sendData(endpoint: string, data: Uint8Array): Promise<Uint8Array> {
|
|
46
|
+
if (!this.connected) {
|
|
47
|
+
throw new Error('Not connected to device');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(`${this.baseUrl}/${endpoint}`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Accept': 'text/plain',
|
|
55
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
56
|
+
},
|
|
57
|
+
body: data.buffer as ArrayBuffer,
|
|
58
|
+
mode: 'cors',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(`HTTP ${response.status}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
66
|
+
return new Uint8Array(arrayBuffer);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new Error(`SoftAP communication failed: ${error}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isConnected(): boolean {
|
|
73
|
+
return this.connected;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set custom base URL
|
|
78
|
+
*/
|
|
79
|
+
setBaseUrl(url: string): void {
|
|
80
|
+
this.baseUrl = url;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get current base URL
|
|
85
|
+
*/
|
|
86
|
+
getBaseUrl(): string {
|
|
87
|
+
return this.baseUrl;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for transport implementations
|
|
3
|
+
*/
|
|
4
|
+
export abstract class Transport {
|
|
5
|
+
/**
|
|
6
|
+
* Connect to the device
|
|
7
|
+
*/
|
|
8
|
+
abstract connect(): Promise<void>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Disconnect from the device
|
|
12
|
+
*/
|
|
13
|
+
abstract disconnect(): void;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Send data to a specific endpoint
|
|
17
|
+
*/
|
|
18
|
+
abstract sendData(endpoint: string, data: Uint8Array): Promise<Uint8Array>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if transport is connected
|
|
22
|
+
*/
|
|
23
|
+
abstract isConnected(): boolean;
|
|
24
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type ESPSecurityConfig = ESPSecurity1Config | ESPSecurity2Config;
|
|
2
|
+
|
|
3
|
+
export interface ESPSecurity1Config {
|
|
4
|
+
type: 'Security1';
|
|
5
|
+
proofOfPossession: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ESPSecurity2Config {
|
|
9
|
+
type: 'Security2';
|
|
10
|
+
username: string;
|
|
11
|
+
password: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* WiFi authentication modes
|
|
16
|
+
*/
|
|
17
|
+
export enum ESPWifiAuthMode {
|
|
18
|
+
open = 0,
|
|
19
|
+
wep = 1,
|
|
20
|
+
wpa2Enterprise = 2,
|
|
21
|
+
wpa2Psk = 3,
|
|
22
|
+
wpaPsk = 4,
|
|
23
|
+
wpaWpa2Psk = 5,
|
|
24
|
+
wpa3Psk = 6,
|
|
25
|
+
wpa2Wpa3Psk = 7,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* WiFi STA state
|
|
30
|
+
*/
|
|
31
|
+
export enum ESPWifiStaState {
|
|
32
|
+
connected = 0,
|
|
33
|
+
connecting = 1,
|
|
34
|
+
disconnected = 2,
|
|
35
|
+
failed = 3,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* WiFi scan failure reasons
|
|
40
|
+
*/
|
|
41
|
+
export enum ESPConnectFailReason {
|
|
42
|
+
authError = 1,
|
|
43
|
+
networkNotFound = 2,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ESPWifiStatus {
|
|
47
|
+
staState: ESPWifiStaState;
|
|
48
|
+
failedReason?: ESPConnectFailReason;
|
|
49
|
+
connected?: ESPWifiAp;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* WiFi network information returned by device scan
|
|
54
|
+
*/
|
|
55
|
+
export interface ESPWifiAp {
|
|
56
|
+
ssid: string;
|
|
57
|
+
rssi: number;
|
|
58
|
+
auth: ESPWifiAuthMode;
|
|
59
|
+
bssid: string;
|
|
60
|
+
channel: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Scan status response
|
|
65
|
+
*/
|
|
66
|
+
export interface ESPScanStatusResponse {
|
|
67
|
+
scanFinished: boolean;
|
|
68
|
+
count: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Device interface for ESP provisioning
|
|
73
|
+
*/
|
|
74
|
+
export interface ESPDeviceInterface {
|
|
75
|
+
connect(security?: ESPSecurityConfig): Promise<void>;
|
|
76
|
+
|
|
77
|
+
scanWifiList(): Promise<ESPWifiAp[]>;
|
|
78
|
+
|
|
79
|
+
disconnect(): void;
|
|
80
|
+
|
|
81
|
+
provision(ssid: string, passphrase: string): Promise<void>;
|
|
82
|
+
|
|
83
|
+
fetchWifiStatus(): Promise<ESPWifiStatus>;
|
|
84
|
+
|
|
85
|
+
getDevice(): BluetoothDevice | URL;
|
|
86
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic utilities for ESP provisioning protocol
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate random bytes
|
|
7
|
+
*/
|
|
8
|
+
export function generateRandomBytes(length: number): Uint8Array {
|
|
9
|
+
const bytes = new Uint8Array(length);
|
|
10
|
+
crypto.getRandomValues(bytes);
|
|
11
|
+
return bytes;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert hex string to Uint8Array
|
|
16
|
+
*/
|
|
17
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
18
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
19
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
20
|
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
|
21
|
+
}
|
|
22
|
+
return bytes;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert Uint8Array to hex string
|
|
27
|
+
*/
|
|
28
|
+
export function bytesToHex(bytes: Uint8Array): string {
|
|
29
|
+
return Array.from(bytes)
|
|
30
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
31
|
+
.join('');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert string to Uint8Array
|
|
36
|
+
*/
|
|
37
|
+
export function stringToBytes(str: string): Uint8Array {
|
|
38
|
+
return new TextEncoder().encode(str);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert Uint8Array to string
|
|
43
|
+
*/
|
|
44
|
+
export function bytesToString(bytes: Uint8Array): string {
|
|
45
|
+
return new TextDecoder().decode(bytes);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* SHA-256 hash function
|
|
50
|
+
*/
|
|
51
|
+
export async function sha256(data: Uint8Array): Promise<Uint8Array> {
|
|
52
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data.buffer as ArrayBuffer);
|
|
53
|
+
return new Uint8Array(hashBuffer);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* HMAC-SHA-256
|
|
58
|
+
*/
|
|
59
|
+
export async function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
|
|
60
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
61
|
+
'raw',
|
|
62
|
+
key.buffer as ArrayBuffer,
|
|
63
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
64
|
+
false,
|
|
65
|
+
['sign']
|
|
66
|
+
);
|
|
67
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, data.buffer as ArrayBuffer);
|
|
68
|
+
return new Uint8Array(signature);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* AES-CTR encryption
|
|
73
|
+
*/
|
|
74
|
+
export async function aesEncrypt(
|
|
75
|
+
key: Uint8Array,
|
|
76
|
+
counter: Uint8Array,
|
|
77
|
+
data: Uint8Array
|
|
78
|
+
): Promise<Uint8Array> {
|
|
79
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
80
|
+
'raw',
|
|
81
|
+
key.buffer as ArrayBuffer,
|
|
82
|
+
{ name: 'AES-CTR' },
|
|
83
|
+
false,
|
|
84
|
+
['encrypt']
|
|
85
|
+
);
|
|
86
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
87
|
+
{ name: 'AES-CTR', counter: counter.buffer as ArrayBuffer, length: 128 },
|
|
88
|
+
cryptoKey,
|
|
89
|
+
data.buffer as ArrayBuffer
|
|
90
|
+
);
|
|
91
|
+
return new Uint8Array(encrypted);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* AES-CTR decryption
|
|
96
|
+
*/
|
|
97
|
+
export async function aesDecrypt(
|
|
98
|
+
key: Uint8Array,
|
|
99
|
+
counter: Uint8Array,
|
|
100
|
+
data: Uint8Array
|
|
101
|
+
): Promise<Uint8Array> {
|
|
102
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
103
|
+
'raw',
|
|
104
|
+
key.buffer as ArrayBuffer,
|
|
105
|
+
{ name: 'AES-CTR' },
|
|
106
|
+
false,
|
|
107
|
+
['decrypt']
|
|
108
|
+
);
|
|
109
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
110
|
+
{ name: 'AES-CTR', counter: counter.buffer as ArrayBuffer, length: 128 },
|
|
111
|
+
cryptoKey,
|
|
112
|
+
data.buffer as ArrayBuffer
|
|
113
|
+
);
|
|
114
|
+
return new Uint8Array(decrypted);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Concatenate multiple Uint8Arrays
|
|
119
|
+
*/
|
|
120
|
+
export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
|
|
121
|
+
const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0);
|
|
122
|
+
const result = new Uint8Array(totalLength);
|
|
123
|
+
let offset = 0;
|
|
124
|
+
for (const arr of arrays) {
|
|
125
|
+
result.set(arr, offset);
|
|
126
|
+
offset += arr.length;
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|