esp-idf-provisioning-web 0.0.3 → 0.0.6

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.
Files changed (43) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +84 -21
  3. package/dist/ESPDevice.d.ts +4 -19
  4. package/dist/ESPDevice.d.ts.map +1 -1
  5. package/dist/index.cjs.js +1 -1
  6. package/dist/index.d.ts +4 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.es.js +4623 -3094
  9. package/dist/security/Security0.d.ts.map +1 -1
  10. package/dist/security/Security1.d.ts +17 -0
  11. package/dist/security/Security1.d.ts.map +1 -0
  12. package/dist/types.d.ts +15 -1
  13. package/dist/types.d.ts.map +1 -1
  14. package/dist/utils/crypto.d.ts +9 -0
  15. package/dist/utils/crypto.d.ts.map +1 -1
  16. package/package.json +26 -24
  17. package/src/ESPDevice.ts +79 -74
  18. package/src/ESPProvisionManager.ts +0 -0
  19. package/src/constants.ts +0 -0
  20. package/src/generated/.gitkeep +0 -0
  21. package/src/generated/proto.d.ts +0 -0
  22. package/src/generated/proto.js +1864 -659
  23. package/src/index.ts +9 -4
  24. package/src/proto/constants.proto +0 -0
  25. package/src/proto/sec0.proto +0 -0
  26. package/src/proto/sec1.proto +0 -0
  27. package/src/proto/sec2.proto +0 -0
  28. package/src/proto/session.proto +0 -0
  29. package/src/proto/wifi_config.proto +0 -0
  30. package/src/proto/wifi_constants.proto +0 -0
  31. package/src/proto/wifi_ctrl.proto +0 -0
  32. package/src/proto/wifi_scan.proto +0 -0
  33. package/src/protocol/protocol.ts +0 -0
  34. package/src/protocol/wifiConfig.ts +6 -6
  35. package/src/protocol/wifiScan.ts +4 -4
  36. package/src/security/Security.ts +0 -0
  37. package/src/security/Security0.ts +5 -3
  38. package/src/security/Security1.ts +110 -0
  39. package/src/transport/BLETransport.ts +0 -0
  40. package/src/transport/SoftAPTransport.ts +2 -2
  41. package/src/transport/Transport.ts +0 -0
  42. package/src/types.ts +17 -1
  43. package/src/utils/crypto.ts +43 -9
package/src/index.ts CHANGED
@@ -1,13 +1,15 @@
1
+ /// <reference types="web-bluetooth" preserve="true" />
2
+
1
3
  /**
2
4
  * ESP-IDF Provisioning Web SDK
3
5
  *
4
6
  * A TypeScript library for provisioning ESP32 devices via Web Bluetooth API and HTTP.
5
- * Supports both BLE and SoftAP transport modes with Security0, Security1, and Security2.
7
+ * Supports both BLE and SoftAP transport modes with Security0 and Security1.
6
8
  *
7
9
  * Note: Browser limitations apply:
8
10
  * - BLE requires user interaction to select devices
9
11
  * - SoftAP requires manual WiFi connection by the user
10
- * - Some security features may require additional crypto libraries for production use
12
+ * - Security1 requires Web Crypto with X25519 support (HTTPS or localhost)
11
13
  */
12
14
 
13
15
  export { ESPDevice } from './ESPDevice';
@@ -16,12 +18,15 @@ export { ESPProvisionManager } from './ESPProvisionManager';
16
18
  export {
17
19
  ESPWifiAuthMode,
18
20
  ESPWifiStaState,
21
+ ESPConnectFailReason,
19
22
  type ESPDeviceInterface,
23
+ type ESPSecurityConfig,
24
+ type ESPSecurityResolver,
25
+ type ESPProvisioningInfo,
26
+ type ESPProvisioningMetadata,
20
27
  type ESPSecurity1Config,
21
28
  type ESPSecurity2Config,
22
29
  type ESPWifiAp,
23
- type ESPScanStatusResponse as ESPStatusResponse,
24
- type ESPConnectFailReason,
25
30
  type ESPWifiStatus,
26
31
  } from './types';
27
32
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -4,7 +4,7 @@ import { bytesToHex, bytesToString, stringToBytes } from '../utils/crypto.js';
4
4
  import { wifiAuthModeToESPWifiAuthMode } from './protocol.js';
5
5
 
6
6
  export function getConfigStatusRequest(): Uint8Array {
7
- let configRequest = proto.WiFiConfigPayload.create({});
7
+ const configRequest = proto.WiFiConfigPayload.create({});
8
8
  configRequest.msg = proto.WiFiConfigMsgType.TypeCmdGetStatus;
9
9
  configRequest.cmdGetStatus = proto.CmdGetStatus.create({});
10
10
  return proto.WiFiConfigPayload.encode(configRequest).finish();
@@ -61,13 +61,13 @@ export function processConfigStatusResponse(response: Uint8Array): ESPWifiStatus
61
61
  failedReason,
62
62
  connected,
63
63
  };
64
- } catch (error) {
64
+ } catch {
65
65
  throw new Error('Failed to decode config status response');
66
66
  }
67
67
  }
68
68
 
69
69
  export function getConfigUpdateRequest(ssid: string, passphrase: string): Uint8Array {
70
- let configRequest = proto.WiFiConfigPayload.create({});
70
+ const configRequest = proto.WiFiConfigPayload.create({});
71
71
  configRequest.msg = proto.WiFiConfigMsgType.TypeCmdSetConfig;
72
72
  configRequest.cmdSetConfig = proto.CmdSetConfig.create({
73
73
  ssid: stringToBytes(ssid),
@@ -83,13 +83,13 @@ export function processConfigUpdateResponse(response: Uint8Array): void {
83
83
  if (result.status !== proto.Status.Success) {
84
84
  throw new Error(`Config update failed: ${result.status}`);
85
85
  }
86
- } catch (error) {
86
+ } catch {
87
87
  throw new Error('Failed to decode config update response');
88
88
  }
89
89
  }
90
90
 
91
91
  export function getConfigApplyRequest(): Uint8Array {
92
- let configRequest = proto.WiFiConfigPayload.create({});
92
+ const configRequest = proto.WiFiConfigPayload.create({});
93
93
  configRequest.msg = proto.WiFiConfigMsgType.TypeCmdApplyConfig;
94
94
  configRequest.cmdApplyConfig = proto.CmdApplyConfig.create({});
95
95
  return proto.WiFiConfigPayload.encode(configRequest).finish();
@@ -102,7 +102,7 @@ export function processConfigApplyResponse(response: Uint8Array): void {
102
102
  if (result.status !== proto.Status.Success) {
103
103
  throw new Error(`Config apply failed: ${result.status}`);
104
104
  }
105
- } catch (error) {
105
+ } catch {
106
106
  throw new Error('Failed to decode config apply response');
107
107
  }
108
108
  }
@@ -9,7 +9,7 @@ export function getScanStartRequest(
9
9
  groupChannels: number = 5,
10
10
  periodMs: number = 120
11
11
  ): Uint8Array {
12
- let cmd = proto.CmdScanStart.create({});
12
+ const cmd = proto.CmdScanStart.create({});
13
13
  cmd.blocking = blocking;
14
14
  cmd.passive = passive;
15
15
  cmd.groupChannels = groupChannels;
@@ -51,13 +51,13 @@ export function processScanStatusResponse(response: Uint8Array): ESPScanStatusRe
51
51
  scanFinished: result.scanFinished,
52
52
  count: result.resultCount,
53
53
  };
54
- } catch (error) {
54
+ } catch {
55
55
  throw new Error('Failed to decode scan status response');
56
56
  }
57
57
  }
58
58
 
59
59
  export function getScanResultRequest(index: number, count: number): Uint8Array {
60
- let cmd = proto.CmdScanResult.create({});
60
+ const cmd = proto.CmdScanResult.create({});
61
61
  cmd.startIndex = index;
62
62
  cmd.count = count;
63
63
  const scanRequest = proto.WiFiScanPayload.create({
@@ -85,7 +85,7 @@ export function processScanResultResponse(response: Uint8Array): ESPWifiAp[] {
85
85
  };
86
86
  });
87
87
  return wifiList;
88
- } catch (error) {
88
+ } catch {
89
89
  throw new Error('Failed to decode scan result response');
90
90
  }
91
91
  }
File without changes
@@ -16,18 +16,20 @@ export class Security0 extends Security {
16
16
  }
17
17
 
18
18
  async getSessionSetupRequest(): Promise<Uint8Array> {
19
- let setupReq = proto.SessionData.create({});
19
+ const setupReq = proto.SessionData.create({});
20
20
  setupReq.secVer = proto.SecSchemeVersion.SecScheme0;
21
21
  setupReq.sec0 = proto.Sec0Payload.create({
22
22
  msg: proto.Sec0MsgType.S0_Session_Command,
23
23
  sc: proto.S0SessionCmd.create({})
24
24
  });
25
- return proto.Sec0Payload.encode(setupReq).finish();
25
+ return proto.SessionData.encode(setupReq).finish();
26
26
  }
27
27
 
28
28
  async processSessionSetupResponse(response: Uint8Array): Promise<void> {
29
29
  const setupResp = proto.SessionData.decode(response);
30
- if (setupResp.secVer === proto.SecSchemeVersion.SecScheme0 && setupResp.sec0) {
30
+ if (setupResp.secVer === proto.SecSchemeVersion.SecScheme0 &&
31
+ setupResp.sec0?.msg === proto.Sec0MsgType.S0_Session_Response &&
32
+ setupResp.sec0.sr?.status === proto.Status.Success) {
31
33
  this.established = true;
32
34
  } else {
33
35
  throw new Error('Invalid session setup response for Security0');
@@ -0,0 +1,110 @@
1
+ import { Security } from './Security';
2
+ import * as proto from '../generated/proto.js';
3
+ import { AESCTRStream, sha256, stringToBytes } from '../utils/crypto';
4
+
5
+ /** ESP-IDF Security 1: X25519, optional PoP, and one continuous AES-CTR stream. */
6
+ export class Security1 extends Security {
7
+ private phase: 'command0' | 'response0' | 'command1' | 'response1' | 'established' = 'command0';
8
+ private keyPair?: CryptoKeyPair;
9
+ private publicKey?: Uint8Array;
10
+ private devicePublicKey?: Uint8Array;
11
+ private cipher?: AESCTRStream;
12
+
13
+ constructor(private proofOfPossession: string, patchVersion?: number) {
14
+ super();
15
+ if ((patchVersion ?? 0) !== 0) {
16
+ throw new Error('Unsupported Security1 patch version');
17
+ }
18
+ if (typeof proofOfPossession !== 'string') {
19
+ throw new Error('Unsupported security configuration');
20
+ }
21
+ }
22
+
23
+ async getSessionSetupRequest(): Promise<Uint8Array> {
24
+ if (this.phase === 'command0') {
25
+ this.keyPair = await crypto.subtle.generateKey('X25519', false, ['deriveBits']) as CryptoKeyPair;
26
+ this.publicKey = new Uint8Array(await crypto.subtle.exportKey('raw', this.keyPair.publicKey));
27
+ this.phase = 'response0';
28
+ return proto.SessionData.encode({
29
+ secVer: proto.SecSchemeVersion.SecScheme1,
30
+ sec1: {
31
+ msg: proto.Sec1MsgType.Session_Command0,
32
+ sc0: { clientPubkey: this.publicKey },
33
+ },
34
+ }).finish();
35
+ }
36
+ if (this.phase === 'command1' && this.cipher && this.devicePublicKey) {
37
+ const clientVerifyData = await this.cipher.update(this.devicePublicKey);
38
+ this.phase = 'response1';
39
+ return proto.SessionData.encode({
40
+ secVer: proto.SecSchemeVersion.SecScheme1,
41
+ sec1: { msg: proto.Sec1MsgType.Session_Command1, sc1: { clientVerifyData } },
42
+ }).finish();
43
+ }
44
+ throw new Error('Invalid Security1 handshake state');
45
+ }
46
+
47
+ async processSessionSetupResponse(response: Uint8Array): Promise<void> {
48
+ const session = proto.SessionData.decode(response);
49
+ if (session.secVer !== proto.SecSchemeVersion.SecScheme1 || !session.sec1) {
50
+ throw new Error('Invalid Security1 session response');
51
+ }
52
+ const payload = session.sec1;
53
+ if (this.phase === 'response0' && this.keyPair) {
54
+ const reply = payload.sr0;
55
+ if (payload.msg !== proto.Sec1MsgType.Session_Response0 || !reply ||
56
+ reply.status !== proto.Status.Success || reply.devicePubkey?.length !== 32 ||
57
+ reply.deviceRandom?.length !== 16) {
58
+ throw new Error('Invalid Security1 key exchange response');
59
+ }
60
+ this.devicePublicKey = new Uint8Array(reply.devicePubkey);
61
+ const peerKey = await crypto.subtle.importKey(
62
+ 'raw', new Uint8Array(this.devicePublicKey), 'X25519', false, []
63
+ );
64
+ const sharedKey = new Uint8Array(await crypto.subtle.deriveBits(
65
+ { name: 'X25519', public: peerKey }, this.keyPair.privateKey, 256
66
+ ));
67
+ this.keyPair = undefined;
68
+ try {
69
+ if (this.proofOfPossession.length > 0) {
70
+ const hash = await sha256(stringToBytes(this.proofOfPossession));
71
+ for (let i = 0; i < sharedKey.length; i++) sharedKey[i] ^= hash[i];
72
+ hash.fill(0);
73
+ }
74
+ this.proofOfPossession = '';
75
+ this.cipher = await AESCTRStream.create(sharedKey, reply.deviceRandom);
76
+ } finally {
77
+ sharedKey.fill(0);
78
+ }
79
+ this.phase = 'command1';
80
+ return;
81
+ }
82
+ if (this.phase === 'response1' && this.cipher && this.publicKey) {
83
+ const reply = payload.sr1;
84
+ if (payload.msg !== proto.Sec1MsgType.Session_Response1 || !reply ||
85
+ reply.status !== proto.Status.Success || reply.deviceVerifyData?.length !== 32) {
86
+ throw new Error('Security1 authentication failed');
87
+ }
88
+ const verified = await this.cipher.update(reply.deviceVerifyData);
89
+ let difference = 0;
90
+ for (let i = 0; i < this.publicKey.length; i++) difference |= verified[i] ^ this.publicKey[i];
91
+ if (difference !== 0) throw new Error('Security1 device verification failed');
92
+ this.phase = 'established';
93
+ return;
94
+ }
95
+ throw new Error('Unexpected Security1 handshake response');
96
+ }
97
+
98
+ async encrypt(data: Uint8Array): Promise<Uint8Array> {
99
+ if (!this.isEstablished() || !this.cipher) throw new Error('Security1 session not established');
100
+ return this.cipher.update(data);
101
+ }
102
+
103
+ async decrypt(data: Uint8Array): Promise<Uint8Array> {
104
+ return this.encrypt(data);
105
+ }
106
+
107
+ isEstablished(): boolean {
108
+ return this.phase === 'established';
109
+ }
110
+ }
File without changes
@@ -14,7 +14,7 @@ export class SoftAPTransport extends Transport {
14
14
  */
15
15
  constructor(baseUrl: string) {
16
16
  super();
17
- this.baseUrl = baseUrl;
17
+ this.baseUrl = baseUrl.replace(/\/$/, '');
18
18
  }
19
19
 
20
20
  async connect(): Promise<void> {
@@ -54,7 +54,7 @@ export class SoftAPTransport extends Transport {
54
54
  'Accept': 'text/plain',
55
55
  'Content-Type': 'application/x-www-form-urlencoded',
56
56
  },
57
- body: data.buffer as ArrayBuffer,
57
+ body: new Uint8Array(data),
58
58
  mode: 'cors',
59
59
  });
60
60
 
File without changes
package/src/types.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /** Public proto-ver metadata, including application-defined capability lists. */
2
+ export interface ESPProvisioningMetadata {
3
+ ver?: string;
4
+ cap?: string[];
5
+ }
6
+
7
+ export interface ESPProvisioningInfo {
8
+ prov: ESPProvisioningMetadata & { sec_ver?: number; sec_patch_ver?: number };
9
+ [name: string]: ESPProvisioningMetadata;
10
+ }
11
+
12
+ /** Resolve the security configuration after reading public discovery data. */
13
+ export type ESPSecurityResolver = (
14
+ info: ESPProvisioningInfo
15
+ ) => ESPSecurityConfig | undefined | Promise<ESPSecurityConfig | undefined>;
16
+
1
17
  export type ESPSecurityConfig = ESPSecurity1Config | ESPSecurity2Config;
2
18
 
3
19
  export interface ESPSecurity1Config {
@@ -72,7 +88,7 @@ export interface ESPScanStatusResponse {
72
88
  * Device interface for ESP provisioning
73
89
  */
74
90
  export interface ESPDeviceInterface {
75
- connect(security?: ESPSecurityConfig): Promise<void>;
91
+ connect(security?: ESPSecurityConfig | ESPSecurityResolver): Promise<void>;
76
92
 
77
93
  scanWifiList(): Promise<ESPWifiAp[]>;
78
94
 
@@ -49,7 +49,7 @@ export function bytesToString(bytes: Uint8Array): string {
49
49
  * SHA-256 hash function
50
50
  */
51
51
  export async function sha256(data: Uint8Array): Promise<Uint8Array> {
52
- const hashBuffer = await crypto.subtle.digest('SHA-256', data.buffer as ArrayBuffer);
52
+ const hashBuffer = await crypto.subtle.digest('SHA-256', new Uint8Array(data));
53
53
  return new Uint8Array(hashBuffer);
54
54
  }
55
55
 
@@ -59,12 +59,12 @@ export async function sha256(data: Uint8Array): Promise<Uint8Array> {
59
59
  export async function hmacSha256(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
60
60
  const cryptoKey = await crypto.subtle.importKey(
61
61
  'raw',
62
- key.buffer as ArrayBuffer,
62
+ new Uint8Array(key),
63
63
  { name: 'HMAC', hash: 'SHA-256' },
64
64
  false,
65
65
  ['sign']
66
66
  );
67
- const signature = await crypto.subtle.sign('HMAC', cryptoKey, data.buffer as ArrayBuffer);
67
+ const signature = await crypto.subtle.sign('HMAC', cryptoKey, new Uint8Array(data));
68
68
  return new Uint8Array(signature);
69
69
  }
70
70
 
@@ -78,15 +78,15 @@ export async function aesEncrypt(
78
78
  ): Promise<Uint8Array> {
79
79
  const cryptoKey = await crypto.subtle.importKey(
80
80
  'raw',
81
- key.buffer as ArrayBuffer,
81
+ new Uint8Array(key),
82
82
  { name: 'AES-CTR' },
83
83
  false,
84
84
  ['encrypt']
85
85
  );
86
86
  const encrypted = await crypto.subtle.encrypt(
87
- { name: 'AES-CTR', counter: counter.buffer as ArrayBuffer, length: 128 },
87
+ { name: 'AES-CTR', counter: new Uint8Array(counter), length: 128 },
88
88
  cryptoKey,
89
- data.buffer as ArrayBuffer
89
+ new Uint8Array(data)
90
90
  );
91
91
  return new Uint8Array(encrypted);
92
92
  }
@@ -101,15 +101,15 @@ export async function aesDecrypt(
101
101
  ): Promise<Uint8Array> {
102
102
  const cryptoKey = await crypto.subtle.importKey(
103
103
  'raw',
104
- key.buffer as ArrayBuffer,
104
+ new Uint8Array(key),
105
105
  { name: 'AES-CTR' },
106
106
  false,
107
107
  ['decrypt']
108
108
  );
109
109
  const decrypted = await crypto.subtle.decrypt(
110
- { name: 'AES-CTR', counter: counter.buffer as ArrayBuffer, length: 128 },
110
+ { name: 'AES-CTR', counter: new Uint8Array(counter), length: 128 },
111
111
  cryptoKey,
112
- data.buffer as ArrayBuffer
112
+ new Uint8Array(data)
113
113
  );
114
114
  return new Uint8Array(decrypted);
115
115
  }
@@ -127,3 +127,37 @@ export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
127
127
  }
128
128
  return result;
129
129
  }
130
+
131
+ /** Stateful AES-CTR, preserving partial blocks across requests and responses. */
132
+ export class AESCTRStream {
133
+ private offset = 0;
134
+
135
+ private constructor(private key: CryptoKey, private counter: Uint8Array<ArrayBuffer>) {}
136
+
137
+ static async create(key: Uint8Array, counter: Uint8Array): Promise<AESCTRStream> {
138
+ if (key.length !== 32 || counter.length !== 16) throw new Error('Invalid AES-256-CTR parameters');
139
+ const cryptoKey = await crypto.subtle.importKey(
140
+ 'raw', new Uint8Array(key), 'AES-CTR', false, ['encrypt']
141
+ );
142
+ return new AESCTRStream(cryptoKey, new Uint8Array(counter));
143
+ }
144
+
145
+ async update(data: Uint8Array): Promise<Uint8Array> {
146
+ // Web Crypto restarts at a block boundary. Skip the already-consumed bytes
147
+ // of the current block to match mbedTLS's continuous stream in both directions.
148
+ const input = new Uint8Array(this.offset + data.length);
149
+ input.set(data, this.offset);
150
+ const output = await crypto.subtle.encrypt(
151
+ { name: 'AES-CTR', counter: this.counter, length: 128 }, this.key, input
152
+ );
153
+ const result = new Uint8Array(output).slice(this.offset);
154
+ let blocks = Math.floor(input.length / 16);
155
+ for (let i = this.counter.length - 1; i >= 0 && blocks > 0; i--) {
156
+ const sum = this.counter[i] + blocks;
157
+ this.counter[i] = sum & 0xff;
158
+ blocks = Math.floor(sum / 256);
159
+ }
160
+ this.offset = input.length % 16;
161
+ return result;
162
+ }
163
+ }