odelic 0.0.0-stage → 0.2.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -2
  3. package/bin/odelic-mcp.js +5 -0
  4. package/bin/odelic.js +6 -0
  5. package/dist/cli.d.ts +58 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +279 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/client.d.ts +125 -0
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +354 -0
  12. package/dist/client.js.map +1 -0
  13. package/dist/config.d.ts +34 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +139 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/controller.d.ts +95 -0
  18. package/dist/controller.d.ts.map +1 -0
  19. package/dist/controller.js +210 -0
  20. package/dist/controller.js.map +1 -0
  21. package/dist/crypto.d.ts +25 -0
  22. package/dist/crypto.d.ts.map +1 -0
  23. package/dist/crypto.js +94 -0
  24. package/dist/crypto.js.map +1 -0
  25. package/dist/errors.d.ts +14 -0
  26. package/dist/errors.d.ts.map +1 -0
  27. package/dist/errors.js +17 -0
  28. package/dist/errors.js.map +1 -0
  29. package/dist/index.d.ts +18 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +12 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/log.d.ts +18 -0
  34. package/dist/log.d.ts.map +1 -0
  35. package/dist/log.js +21 -0
  36. package/dist/log.js.map +1 -0
  37. package/dist/mcp.d.ts +23 -0
  38. package/dist/mcp.d.ts.map +1 -0
  39. package/dist/mcp.js +74 -0
  40. package/dist/mcp.js.map +1 -0
  41. package/dist/protocol.d.ts +90 -0
  42. package/dist/protocol.d.ts.map +1 -0
  43. package/dist/protocol.js +178 -0
  44. package/dist/protocol.js.map +1 -0
  45. package/dist/server.d.ts +24 -0
  46. package/dist/server.d.ts.map +1 -0
  47. package/dist/server.js +187 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/version.d.ts +2 -0
  50. package/dist/version.d.ts.map +1 -0
  51. package/dist/version.js +2 -0
  52. package/dist/version.js.map +1 -0
  53. package/package.json +80 -4
  54. package/src/cli.ts +267 -0
  55. package/src/client.ts +414 -0
  56. package/src/config.ts +142 -0
  57. package/src/controller.ts +267 -0
  58. package/src/crypto.ts +100 -0
  59. package/src/errors.ts +17 -0
  60. package/src/index.ts +17 -0
  61. package/src/log.ts +24 -0
  62. package/src/mcp.ts +100 -0
  63. package/src/protocol.ts +205 -0
  64. package/src/server.ts +195 -0
  65. package/src/version.ts +1 -0
package/dist/config.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * User configuration: app ID, preferred node, connection mode and light names.
3
+ *
4
+ * Stored as JSON at $ODELIC_CONFIG, or $XDG_CONFIG_HOME/odelic/config.json (default ~/.config/odelic/config.json).
5
+ * The format is shared with the Python package (odelic.py), so both can use the same file:
6
+ *
7
+ * {"id": "12345678", "node": null, "idle_timeout": 60, "lights": {"living": "00000000"}}
8
+ *
9
+ * idle_timeout: seconds of inactivity before the BLE link is released (so the phone app can connect);
10
+ * 0 keeps the link open permanently.
11
+ */
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { InputError } from './errors.js';
16
+ import * as P from './protocol.js';
17
+ export const ALL_NAMES = ['all', '*', '全部', 'すべて'];
18
+ export const DEFAULT_IDLE_TIMEOUT = 60;
19
+ const KEEP_WORDS = ['0', 'never', 'none', 'off', 'keep', 'forever'];
20
+ /** Seconds before an idle connection is closed; 0 (or "never"/"keep") = stay connected; null = not set. */
21
+ export function parseIdle(value) {
22
+ if (value === null || value === undefined || value === '')
23
+ return null;
24
+ if (typeof value === 'string' && KEEP_WORDS.includes(value.trim().toLowerCase()))
25
+ return 0;
26
+ const v = typeof value === 'string' ? Number(value.trim()) : Number(value);
27
+ if (typeof value === 'boolean' || !Number.isFinite(v))
28
+ throw new InputError(`invalid idle timeout: ${String(value)}`);
29
+ if (v < 0)
30
+ throw new InputError('idle timeout must be >= 0');
31
+ return v;
32
+ }
33
+ export function configPath() {
34
+ if (process.env.ODELIC_CONFIG)
35
+ return expandHome(process.env.ODELIC_CONFIG);
36
+ const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
37
+ return path.join(base, 'odelic', 'config.json');
38
+ }
39
+ function expandHome(p) {
40
+ return p === '~' || p.startsWith('~/') ? path.join(os.homedir(), p.slice(1)) : p;
41
+ }
42
+ export class Config {
43
+ path;
44
+ id;
45
+ node;
46
+ idleTimeout;
47
+ /** name -> vaddr hex (null prototype: names like "constructor" are plain keys) */
48
+ lights;
49
+ constructor(data = {}, file = null) {
50
+ data ??= {};
51
+ this.path = file ?? configPath();
52
+ this.id = data.id ?? null;
53
+ this.node = data.node ?? null;
54
+ this.lights = Object.create(null);
55
+ for (const [k, v] of Object.entries(data.lights ?? {}))
56
+ this.lights[String(k)] = String(v).toLowerCase();
57
+ this.idleTimeout = parseIdle(data.idle_timeout);
58
+ }
59
+ static load(file = null) {
60
+ const p = file ?? configPath();
61
+ let data = {};
62
+ try {
63
+ data = JSON.parse(fs.readFileSync(p, 'utf8'));
64
+ }
65
+ catch (e) {
66
+ if (e.code !== 'ENOENT')
67
+ throw e;
68
+ }
69
+ return new Config(data, p);
70
+ }
71
+ save() {
72
+ fs.mkdirSync(path.dirname(this.path), { recursive: true });
73
+ const tmp = this.path.replace(/\.[^./]*$/, '') + '.tmp';
74
+ const data = { id: this.id, node: this.node, idle_timeout: this.idleTimeout, lights: this.lights };
75
+ // the ID is the mesh password: keep the file private
76
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
77
+ try {
78
+ fs.chmodSync(tmp, 0o600);
79
+ }
80
+ catch {
81
+ // not supported on this filesystem
82
+ }
83
+ fs.renameSync(tmp, this.path);
84
+ }
85
+ /** Effective app ID: override > $ODELIC_ID > config. */
86
+ appId(override = null) {
87
+ return override || process.env.ODELIC_ID || this.id;
88
+ }
89
+ /** Effective idle timeout: override > $ODELIC_IDLE_TIMEOUT > config > 60 s. 0 = stay connected. */
90
+ idle(override = null) {
91
+ for (const v of [override, process.env.ODELIC_IDLE_TIMEOUT, this.idleTimeout]) {
92
+ const parsed = parseIdle(v);
93
+ if (parsed !== null)
94
+ return parsed;
95
+ }
96
+ return DEFAULT_IDLE_TIMEOUT;
97
+ }
98
+ // ------------------------------------------------------------ names
99
+ nameOf(vaddr) {
100
+ const hex = Buffer.from(vaddr).toString('hex');
101
+ for (const [name, v] of Object.entries(this.lights))
102
+ if (v === hex)
103
+ return name;
104
+ return null;
105
+ }
106
+ /** light name / "all" / vaddr hex / bytes -> 4-byte vaddr */
107
+ resolve(target) {
108
+ if (target === null || target === undefined)
109
+ return P.BROADCAST;
110
+ if (target instanceof Uint8Array)
111
+ return P.vaddr(target);
112
+ const t = String(target).trim();
113
+ if (t === '' || ALL_NAMES.includes(t.toLowerCase()))
114
+ return P.BROADCAST;
115
+ for (const [name, v] of Object.entries(this.lights)) {
116
+ if (name.toLowerCase() === t.toLowerCase())
117
+ return Buffer.from(v, 'hex');
118
+ }
119
+ try {
120
+ return P.vaddr(t);
121
+ }
122
+ catch {
123
+ const known = Object.keys(this.lights).join(', ') || '(none)';
124
+ throw new InputError(`unknown light ${JSON.stringify(String(target))} (known names: ${known})`);
125
+ }
126
+ }
127
+ setName(name, target) {
128
+ const v = this.resolve(target);
129
+ if (v.equals(P.BROADCAST))
130
+ throw new InputError('cannot name the broadcast address');
131
+ this.lights[name] = v.toString('hex');
132
+ }
133
+ removeName(name) {
134
+ if (!Object.hasOwn(this.lights, name))
135
+ throw new InputError(`no light named ${JSON.stringify(name)}`);
136
+ delete this.lights[name];
137
+ }
138
+ }
139
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import * as P from './protocol.ts';
2
+ import { OdelicMesh } from './client.ts';
3
+ import { Config } from './config.ts';
4
+ /** Where a command goes: a light name from the config, "all", or a vaddr. */
5
+ export type Target = P.VaddrLike;
6
+ export interface ActionResult {
7
+ ok: true;
8
+ action: string;
9
+ target?: string;
10
+ color?: number;
11
+ brightness?: number;
12
+ level?: number;
13
+ group?: number;
14
+ index?: number;
15
+ }
16
+ export interface LightInfo {
17
+ name: string | null;
18
+ vaddr: string;
19
+ online: boolean;
20
+ mac?: string;
21
+ product?: number;
22
+ version?: string;
23
+ }
24
+ export type LightStatus = ({
25
+ name: string | null;
26
+ online: true;
27
+ } & P.LightStateJSON) | {
28
+ name: string | null;
29
+ vaddr: string;
30
+ online: false;
31
+ };
32
+ export interface ControllerInfo {
33
+ connected: boolean;
34
+ node: string | null;
35
+ mode: 'keep' | 'idle';
36
+ idle_timeout: number;
37
+ }
38
+ export interface ControllerOptions {
39
+ /** BLE name / address of the node to connect through (default: config, else strongest node) */
40
+ node?: string | null;
41
+ config?: Config | null;
42
+ /** seconds before an idle link is released; 0 = stay connected; null = $ODELIC_IDLE_TIMEOUT / config / 60 s */
43
+ idleTimeout?: number | string | null;
44
+ /** use this mesh connection instead of creating one (mainly for tests) */
45
+ mesh?: OdelicMesh | null;
46
+ }
47
+ /** Integer from a number or numeric string (like Python's int()); throws InputError otherwise. */
48
+ export declare function toInt(v: unknown, what?: string): number;
49
+ export declare class Controller {
50
+ readonly config: Config;
51
+ readonly mesh: OdelicMesh;
52
+ /** seconds; 0 = keep the link open */
53
+ readonly idleTimeout: number;
54
+ private queue;
55
+ private idle;
56
+ /** @param appId default: $ODELIC_ID / config file */
57
+ constructor(appId?: string | null, { node, config, idleTimeout, mesh }?: ControllerOptions);
58
+ /** Run `fn` after every previously queued operation (a simple mutex). */
59
+ private locked;
60
+ private run;
61
+ private cancelIdle;
62
+ private armIdle;
63
+ /** Connect now (otherwise the first command connects). */
64
+ connect(): Promise<void>;
65
+ close(): Promise<void>;
66
+ get connected(): boolean;
67
+ get keepConnected(): boolean;
68
+ info(): ControllerInfo;
69
+ private label;
70
+ private done;
71
+ on(target?: Target): Promise<ActionResult>;
72
+ off(target?: Target): Promise<ActionResult>;
73
+ /** color 0 (warm) .. 100 (cool), brightness 1..100 % */
74
+ setLight(target?: Target, color?: number | string, brightness?: number | string): Promise<ActionResult>;
75
+ nightLight(target?: Target, level?: number | string): Promise<ActionResult>;
76
+ /** action: "on", "off" or "light" (uses color / brightness) */
77
+ group(groupId: number | string, action?: string, color?: number | string, brightness?: number | string): Promise<ActionResult>;
78
+ scene(index: number | string, save?: boolean): Promise<ActionResult>;
79
+ changeColor(target: Target): Promise<ActionResult>;
80
+ /** Lights answering a mesh ping, plus named lights that did not answer (online: false). */
81
+ lights(timeout?: number): Promise<LightInfo[]>;
82
+ /** State of one light, or of every responding light for "all". */
83
+ status(target?: Target): Promise<LightStatus | LightStatus[]>;
84
+ private statusOf;
85
+ nameLight(name: string, target: Target): {
86
+ ok: true;
87
+ name: string;
88
+ vaddr: string;
89
+ };
90
+ forgetName(name: string): {
91
+ ok: true;
92
+ removed: string;
93
+ };
94
+ }
95
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * High-level controller shared by the CLI, the HTTP API and the MCP server.
3
+ *
4
+ * - connects lazily, serialises operations and reconnects once if the link dropped
5
+ * - disconnects after `idleTimeout` seconds without commands, so the phone app can use the mesh again
6
+ * (0 = keep the link open)
7
+ * - accepts light names from the config file ("living"), "all", or a raw vaddr ("00000000")
8
+ */
9
+ import { InputError, LoginError } from './errors.js';
10
+ import { log } from './log.js';
11
+ import * as P from './protocol.js';
12
+ import { OdelicMesh } from './client.js';
13
+ import { Config } from './config.js';
14
+ /** Integer from a number or numeric string (like Python's int()); throws InputError otherwise. */
15
+ export function toInt(v, what = 'value') {
16
+ const n = typeof v === 'string' && v.trim() !== '' ? Number(v) : v;
17
+ if (typeof n !== 'number' || !Number.isFinite(n))
18
+ throw new InputError(`${what} must be a number (got ${JSON.stringify(v)})`);
19
+ return Math.trunc(n);
20
+ }
21
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
22
+ const byVaddr = (a, b) => Buffer.compare(a.vaddr, b.vaddr);
23
+ export class Controller {
24
+ config;
25
+ mesh;
26
+ /** seconds; 0 = keep the link open */
27
+ idleTimeout;
28
+ queue = Promise.resolve();
29
+ idle = null;
30
+ /** @param appId default: $ODELIC_ID / config file */
31
+ constructor(appId = null, { node = null, config = null, idleTimeout = null, mesh = null } = {}) {
32
+ this.config = config ?? Config.load();
33
+ const aid = this.config.appId(appId);
34
+ if (!aid && !mesh)
35
+ throw new InputError('no app ID: pass --id, set ODELIC_ID, or run `odelic setup --id <ID>`');
36
+ this.mesh = mesh ?? new OdelicMesh(aid, { device: node ?? this.config.node });
37
+ this.idleTimeout = this.config.idle(idleTimeout);
38
+ }
39
+ // ------------------------------------------------------------ connection handling
40
+ /** Run `fn` after every previously queued operation (a simple mutex). */
41
+ locked(fn) {
42
+ const run = this.queue.then(fn);
43
+ this.queue = run.catch(() => { });
44
+ return run;
45
+ }
46
+ run(fn) {
47
+ return this.locked(async () => {
48
+ this.cancelIdle();
49
+ try {
50
+ for (let attempt = 0;; attempt++) {
51
+ if (!this.mesh.isConnected)
52
+ await this.mesh.connect();
53
+ try {
54
+ return await fn(this.mesh);
55
+ }
56
+ catch (e) {
57
+ // retry once when the BLE link dropped under us; input / login errors are final
58
+ if (attempt || this.mesh.isConnected || e instanceof InputError || e instanceof LoginError)
59
+ throw e;
60
+ log.info(`link lost (${e.message}), reconnecting`);
61
+ await this.mesh.disconnect();
62
+ }
63
+ }
64
+ }
65
+ finally {
66
+ this.armIdle();
67
+ }
68
+ });
69
+ }
70
+ cancelIdle() {
71
+ if (this.idle)
72
+ clearTimeout(this.idle);
73
+ this.idle = null;
74
+ }
75
+ armIdle() {
76
+ if (!this.idleTimeout || !this.mesh.isConnected)
77
+ return;
78
+ this.idle = setTimeout(() => {
79
+ this.locked(async () => {
80
+ this.idle = null;
81
+ if (this.mesh.isConnected) {
82
+ log.info('idle, disconnecting');
83
+ await this.mesh.disconnect();
84
+ }
85
+ }).catch((e) => log.warning(`idle disconnect failed: ${e.message}`));
86
+ }, this.idleTimeout * 1000);
87
+ this.idle.unref();
88
+ }
89
+ /** Connect now (otherwise the first command connects). */
90
+ async connect() {
91
+ await this.run(async () => { });
92
+ }
93
+ async close() {
94
+ this.cancelIdle();
95
+ await this.locked(() => this.mesh.disconnect());
96
+ }
97
+ get connected() {
98
+ return this.mesh.isConnected;
99
+ }
100
+ get keepConnected() {
101
+ return !this.idleTimeout;
102
+ }
103
+ info() {
104
+ return { connected: this.connected, node: this.mesh.nodeName, mode: this.keepConnected ? 'keep' : 'idle', idle_timeout: this.idleTimeout };
105
+ }
106
+ // ------------------------------------------------------------ helpers
107
+ label(v) {
108
+ return v.equals(P.BROADCAST) ? 'all' : (this.config.nameOf(v) ?? v.toString('hex'));
109
+ }
110
+ done(action, v, extra = {}) {
111
+ return { ok: true, action, target: this.label(v), ...extra };
112
+ }
113
+ // ------------------------------------------------------------ actions
114
+ async on(target = 'all') {
115
+ const v = this.config.resolve(target);
116
+ await this.run((m) => m.on(v));
117
+ return this.done('on', v);
118
+ }
119
+ async off(target = 'all') {
120
+ const v = this.config.resolve(target);
121
+ await this.run((m) => m.off(v));
122
+ return this.done('off', v);
123
+ }
124
+ /** color 0 (warm) .. 100 (cool), brightness 1..100 % */
125
+ async setLight(target = 'all', color = 50, brightness = 100) {
126
+ const v = this.config.resolve(target);
127
+ const c = clamp(toInt(color, 'color'), 0, 100);
128
+ const b = clamp(toInt(brightness, 'brightness'), 1, 100);
129
+ await this.run((m) => m.setLight({ color: c, brightness: b, target: v }));
130
+ return this.done('light', v, { color: c, brightness: b });
131
+ }
132
+ async nightLight(target = 'all', level = 0) {
133
+ const v = this.config.resolve(target);
134
+ const l = clamp(toInt(level, 'level'), 0, 2);
135
+ await this.run((m) => m.nightLight(l, v));
136
+ return this.done('night', v, { level: l });
137
+ }
138
+ /** action: "on", "off" or "light" (uses color / brightness) */
139
+ async group(groupId, action = 'on', color = 50, brightness = 100) {
140
+ const id = toInt(groupId, 'group id');
141
+ if (action === 'light') {
142
+ const c = clamp(toInt(color, 'color'), 0, 100);
143
+ const b = clamp(toInt(brightness, 'brightness'), 1, 100);
144
+ await this.run((m) => m.setGroup(id, { color: c, brightness: b }));
145
+ }
146
+ else if (action === 'on' || action === 'off') {
147
+ await this.run((m) => m.setGroup(id, { on: action === 'on' }));
148
+ }
149
+ else {
150
+ throw new InputError('action must be on, off or light');
151
+ }
152
+ return { ok: true, action: `group_${action}`, group: id };
153
+ }
154
+ async scene(index, save = false) {
155
+ const i = toInt(index, 'scene index');
156
+ await this.run((m) => (save ? m.saveScene(i) : m.playScene(i)));
157
+ return { ok: true, action: save ? 'scene_save' : 'scene_play', index: i };
158
+ }
159
+ async changeColor(target) {
160
+ const v = this.config.resolve(target);
161
+ await this.run((m) => m.changeColor(v));
162
+ return this.done('change_color', v);
163
+ }
164
+ /** Lights answering a mesh ping, plus named lights that did not answer (online: false). */
165
+ async lights(timeout = 3) {
166
+ const devs = await this.run((m) => m.devices(timeout));
167
+ const out = [];
168
+ const seen = new Set();
169
+ for (const d of [...devs].sort(byVaddr)) {
170
+ const hex = d.vaddr.toString('hex');
171
+ seen.add(hex);
172
+ out.push({ name: this.config.nameOf(d.vaddr), vaddr: hex, online: true, mac: d.mac, product: d.product, version: d.version });
173
+ }
174
+ for (const [name, v] of Object.entries(this.config.lights)) {
175
+ if (!seen.has(v))
176
+ out.push({ name, vaddr: v, online: false });
177
+ }
178
+ return out;
179
+ }
180
+ /** State of one light, or of every responding light for "all". */
181
+ async status(target = 'all') {
182
+ const v = this.config.resolve(target);
183
+ if (v.equals(P.BROADCAST)) {
184
+ const devs = await this.run((m) => m.devices());
185
+ const out = [];
186
+ for (const d of [...devs].sort(byVaddr))
187
+ out.push(await this.statusOf(d.vaddr));
188
+ return out;
189
+ }
190
+ return this.statusOf(v);
191
+ }
192
+ async statusOf(v) {
193
+ const st = await this.run((m) => m.status(v));
194
+ if (st === null)
195
+ return { name: this.config.nameOf(v), vaddr: v.toString('hex'), online: false };
196
+ return { name: this.config.nameOf(v), online: true, ...st.toJSON() };
197
+ }
198
+ // ------------------------------------------------------------ names (no BLE needed)
199
+ nameLight(name, target) {
200
+ this.config.setName(name, target);
201
+ this.config.save();
202
+ return { ok: true, name, vaddr: this.config.lights[name] };
203
+ }
204
+ forgetName(name) {
205
+ this.config.removeName(name);
206
+ this.config.save();
207
+ return { ok: true, removed: name };
208
+ }
209
+ }
210
+ //# sourceMappingURL=controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.js","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,KAAK,CAAC,MAAM,eAAe,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AA8CrC,kGAAkG;AAClG,MAAM,UAAU,KAAK,CAAC,CAAU,EAAE,IAAI,GAAG,OAAO;IAC9C,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,0BAA0B,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC9H,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAU,EAAE,EAAU,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AACnF,MAAM,OAAO,GAAG,CAAC,CAAW,EAAE,CAAW,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;AAE/E,MAAM,OAAO,UAAU;IACZ,MAAM,CAAS;IACf,IAAI,CAAa;IAC1B,sCAAsC;IAC7B,WAAW,CAAS;IACrB,KAAK,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IAC5C,IAAI,GAA0B,IAAI,CAAC;IAE3C,qDAAqD;IACrD,YAAY,KAAK,GAAkB,IAAI,EAAE,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,GAAG,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,GAAsB,EAAE;QAC9H,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,UAAU,CAAC,sEAAsE,CAAC,CAAC;QAChH,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,IAAI,UAAU,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9E,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACnD,CAAC;IAED,mFAAmF;IACnF,yEAAyE;IACjE,MAAM,CAAI,EAAoB;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACjC,OAAO,GAAG,CAAC;IACb,CAAC;IAEO,GAAG,CAAI,EAAoC;QACjD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE;YAC5B,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,IAAI,CAAC;gBACH,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,EAAE,EAAE,CAAC;oBAClC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW;wBAAE,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;oBACtD,IAAI,CAAC;wBACH,OAAO,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC7B,CAAC;oBAAC,OAAO,CAAC,EAAE,CAAC;wBACX,gFAAgF;wBAChF,IAAI,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,YAAY,UAAU,IAAI,CAAC,YAAY,UAAU;4BAAE,MAAM,CAAC,CAAC;wBACpG,GAAG,CAAC,IAAI,CAAC,cAAe,CAAW,CAAC,OAAO,iBAAiB,CAAC,CAAC;wBAC9D,MAAM,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;oBAC/B,CAAC;gBACH,CAAC;YACH,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,UAAU;QAChB,IAAI,IAAI,CAAC,IAAI;YAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAEO,OAAO;QACb,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,OAAO;QACxD,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC,GAAG,EAAE;YAC1B,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE;gBACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;gBACjB,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;oBAC1B,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;oBAChC,MAAM,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;gBAC/B,CAAC;YACH,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QAC9E,CAAC,EAAE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;IAED,0DAA0D;IAC1D,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,GAAE,CAAC,CAAC,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;IAC/B,CAAC;IAED,IAAI,aAAa;QACf,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC;IAC3B,CAAC;IAED,IAAI;QACF,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;IAC7I,CAAC;IAED,uEAAuE;IAC/D,KAAK,CAAC,CAAS;QACrB,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACtF,CAAC;IAEO,IAAI,CAAC,MAAc,EAAE,CAAS,EAAE,KAAK,GAA0B,EAAE;QACvE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC;IAC/D,CAAC;IAED,uEAAuE;IACvE,KAAK,CAAC,EAAE,CAAC,MAAM,GAAW,KAAK;QAC7B,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,MAAM,GAAW,KAAK;QAC9B,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC7B,CAAC;IAED,wDAAwD;IACxD,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAW,KAAK,EAAE,KAAK,GAAoB,EAAE,EAAE,UAAU,GAAoB,GAAG;QACnG,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;QAC/C,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;QACzD,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,MAAM,GAAW,KAAK,EAAE,KAAK,GAAoB,CAAC;QACjE,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7C,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,+DAA+D;IAC/D,KAAK,CAAC,KAAK,CAAC,OAAwB,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAoB,EAAE,EAAE,UAAU,GAAoB,GAAG;QACjH,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QACtC,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;YAC/C,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;YACzD,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACrE,CAAC;aAAM,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YAC/C,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;QACjE,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,UAAU,CAAC,iCAAiC,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAsB,EAAE,IAAI,GAAG,KAAK;QAC9C,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QACtC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,YAAY,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAAc;QAC9B,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,OAAO,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;IACtC,CAAC;IAED,2FAA2F;IAC3F,KAAK,CAAC,MAAM,CAAC,OAAO,GAAG,CAAC;QACtB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QACvD,MAAM,GAAG,GAAgB,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACxC,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YACpC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAChI,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,MAAM,CAAC,MAAM,GAAW,KAAK;QACjC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAChD,MAAM,GAAG,GAAkB,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YAChF,OAAO,GAAG,CAAC;QACb,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IAEO,KAAK,CAAC,QAAQ,CAAC,CAAS;QAC9B,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,EAAE,KAAK,IAAI;YAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACjG,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC;IACvE,CAAC;IAED,qFAAqF;IACrF,SAAS,CAAC,IAAY,EAAE,MAAc;QACpC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACnB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;IAC7D,CAAC;IAED,UAAU,CAAC,IAAY;QACrB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACnB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACrC,CAAC;CACF"}
@@ -0,0 +1,25 @@
1
+ /** AES-128-ECB without padding; `data` must be a multiple of 16 bytes. */
2
+ export declare function aes128(key: Uint8Array, data: Uint8Array, { decrypt }?: {
3
+ decrypt?: boolean;
4
+ }): Buffer;
5
+ export declare function pkcs7Pad(data: Uint8Array): Buffer;
6
+ export declare function pkcs7Unpad(data: Buffer): Buffer;
7
+ /** Keys for one mesh (HomeID + password) and the per-connection session value. */
8
+ export declare class MeshCrypto {
9
+ readonly homeid: Buffer;
10
+ readonly password: Buffer;
11
+ /** Per-connection value from the login challenge; null until logged in. */
12
+ session: Buffer | null;
13
+ private readonly loginKey;
14
+ private readonly eventKey;
15
+ /** @param password 4 ASCII characters */
16
+ constructor(homeid: number, password: string);
17
+ /** `challenge` = the 16 encrypted bytes following `02 20`. Returns the login packet. */
18
+ answerChallenge(challenge: Uint8Array): Buffer;
19
+ private xor;
20
+ /** `03 <dst4> <ch> <payload>` -> `06 <dst4> <ch> <cipher>` */
21
+ encrypt(packet: Uint8Array): Buffer;
22
+ /** `06 <src/dst4> <ch> <cipher>` -> `03 <...> <payload>`; other packet types pass through. */
23
+ decrypt(data: Uint8Array): Buffer;
24
+ }
25
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAiBA,0EAA0E;AAC1E,wBAAgB,MAAM,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,OAAe,EAAE,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAIjH;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAGjD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/C;AAED,kFAAkF;AAClF,qBAAa,UAAU;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,yCAAyC;IACzC,YAAY,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAiB3C;IAED,wFAAwF;IACxF,eAAe,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAQ7C;IAED,OAAO,CAAC,GAAG;IAIX,gEAAgE;IAChE,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAMlC;IAED,gGAAgG;IAChG,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAOhC;CACF"}
package/dist/crypto.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Pairlink connected-mesh crypto.
3
+ *
4
+ * Reverse-engineered from libnative-lib.so in the ODELIC app (jp.co.odelic.smt.remote10):
5
+ *
6
+ * - key material = HomeID (4B, little endian) and password (4 ASCII bytes) interleaved
7
+ * -> h0 p0 h1 p1 h2 p2 h3 p3
8
+ * - login key = material + "LOGINKEY" (AES-128)
9
+ * - event key = material + "EVENTKEY" (AES-128)
10
+ * - login : node sends 02 20 <AES_LK(homeid | session4 | pad)>,
11
+ * we answer 01 16 03 <AES_LK(homeid | pwd | session4 | pad)>
12
+ * - data packets : 03 <dst4> <ch> <payload> -> 06 <dst4> <ch> XOR4(AES_EK(pkcs7(payload)), session4)
13
+ */
14
+ import { createCipheriv, createDecipheriv } from 'node:crypto';
15
+ import { InputError, LoginError } from './errors.js';
16
+ /** AES-128-ECB without padding; `data` must be a multiple of 16 bytes. */
17
+ export function aes128(key, data, { decrypt = false } = {}) {
18
+ const c = decrypt ? createDecipheriv('aes-128-ecb', key, null) : createCipheriv('aes-128-ecb', key, null);
19
+ c.setAutoPadding(false);
20
+ return Buffer.concat([c.update(data), c.final()]);
21
+ }
22
+ export function pkcs7Pad(data) {
23
+ const n = 16 - (data.length % 16);
24
+ return Buffer.concat([data, Buffer.alloc(n, n)]);
25
+ }
26
+ export function pkcs7Unpad(data) {
27
+ const n = data.length ? data[data.length - 1] : 0;
28
+ if (n < 1 || n > 16 || n > data.length || !data.subarray(-n).every((b) => b === n)) {
29
+ throw new InputError('bad padding');
30
+ }
31
+ return data.subarray(0, data.length - n);
32
+ }
33
+ /** Keys for one mesh (HomeID + password) and the per-connection session value. */
34
+ export class MeshCrypto {
35
+ homeid;
36
+ password;
37
+ /** Per-connection value from the login challenge; null until logged in. */
38
+ session = null;
39
+ loginKey;
40
+ eventKey;
41
+ /** @param password 4 ASCII characters */
42
+ constructor(homeid, password) {
43
+ if (!Number.isInteger(homeid) || homeid < 0 || homeid > 0xffffffff) {
44
+ throw new InputError(`HomeID must be a 32-bit unsigned integer (got ${homeid})`);
45
+ }
46
+ if (typeof password !== 'string' || !/^[\x00-\x7f]{4}$/.test(password)) {
47
+ throw new InputError('password must be 4 ASCII characters');
48
+ }
49
+ this.homeid = Buffer.alloc(4);
50
+ this.homeid.writeUInt32LE(homeid);
51
+ this.password = Buffer.from(password, 'ascii');
52
+ const mat = Buffer.alloc(8);
53
+ for (let i = 0; i < 4; i++) {
54
+ mat[2 * i] = this.homeid[i];
55
+ mat[2 * i + 1] = this.password[i];
56
+ }
57
+ this.loginKey = Buffer.concat([mat, Buffer.from('LOGINKEY')]);
58
+ this.eventKey = Buffer.concat([mat, Buffer.from('EVENTKEY')]);
59
+ }
60
+ /** `challenge` = the 16 encrypted bytes following `02 20`. Returns the login packet. */
61
+ answerChallenge(challenge) {
62
+ const plain = aes128(this.loginKey, Buffer.from(challenge).subarray(0, 16), { decrypt: true });
63
+ if (!plain.subarray(0, 4).equals(this.homeid)) {
64
+ throw new LoginError(`node belongs to another HomeID (${plain.readUInt32LE(0)})`);
65
+ }
66
+ this.session = Buffer.from(plain.subarray(4, 8));
67
+ const body = aes128(this.loginKey, pkcs7Pad(Buffer.concat([this.homeid, this.password, this.session])));
68
+ return Buffer.concat([Buffer.from([0x01, 0x16, 0x03]), body]);
69
+ }
70
+ xor(data, key) {
71
+ return Buffer.from(data.map((b, i) => b ^ key[i % 4]));
72
+ }
73
+ /** `03 <dst4> <ch> <payload>` -> `06 <dst4> <ch> <cipher>` */
74
+ encrypt(packet) {
75
+ const plain = Buffer.from(packet);
76
+ if (this.session === null)
77
+ throw new LoginError('not logged in');
78
+ if (plain.length < 7 || plain[0] !== 0x03)
79
+ throw new InputError('expected a data-event packet (type 3)');
80
+ const cipher = aes128(this.eventKey, pkcs7Pad(plain.subarray(6)));
81
+ return Buffer.concat([Buffer.from([0x06]), plain.subarray(1, 6), this.xor(cipher, this.session)]);
82
+ }
83
+ /** `06 <src/dst4> <ch> <cipher>` -> `03 <...> <payload>`; other packet types pass through. */
84
+ decrypt(data) {
85
+ const packet = Buffer.from(data);
86
+ if (!packet.length || packet[0] !== 0x06 || this.session === null)
87
+ return packet;
88
+ let body = this.xor(packet.subarray(6), this.session);
89
+ body = body.subarray(0, body.length - (body.length % 16));
90
+ const plain = pkcs7Unpad(aes128(this.eventKey, body, { decrypt: true }));
91
+ return Buffer.concat([Buffer.from([0x03]), packet.subarray(1, 6), plain]);
92
+ }
93
+ }
94
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /** Error types. `InputError` = bad user input (HTTP 400); the others come from the mesh / BLE side. */
2
+ export declare class OdelicError extends Error {
3
+ constructor(message?: string, options?: ErrorOptions);
4
+ }
5
+ /** Invalid argument: unknown light name, malformed vaddr / ID, out-of-range number, ... */
6
+ export declare class InputError extends OdelicError {
7
+ }
8
+ /** The node rejected the login (wrong HomeID / password, or another controller is connected). */
9
+ export declare class LoginError extends OdelicError {
10
+ }
11
+ /** No mesh node found, or the BLE link could not be established. */
12
+ export declare class ConnectionError extends OdelicError {
13
+ }
14
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,uGAAuG;AAEvG,qBAAa,WAAY,SAAQ,KAAK;IACpC,YAAY,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAGnD;CACF;AAED,2FAA2F;AAC3F,qBAAa,UAAW,SAAQ,WAAW;CAAG;AAE9C,iGAAiG;AACjG,qBAAa,UAAW,SAAQ,WAAW;CAAG;AAE9C,oEAAoE;AACpE,qBAAa,eAAgB,SAAQ,WAAW;CAAG"}