@ecopoesis/homebridge-dmx 0.1.0

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 (226) hide show
  1. package/CLAUDE.md +176 -0
  2. package/README.md +146 -0
  3. package/config.schema.json +143 -0
  4. package/dist/cli/stick.d.ts +3 -0
  5. package/dist/cli/stick.d.ts.map +1 -0
  6. package/dist/cli/stick.js +176 -0
  7. package/dist/cli/stick.js.map +1 -0
  8. package/dist/color/cct.d.ts +3 -0
  9. package/dist/color/cct.d.ts.map +1 -0
  10. package/dist/color/cct.js +33 -0
  11. package/dist/color/cct.js.map +1 -0
  12. package/dist/color/dimmer.d.ts +3 -0
  13. package/dist/color/dimmer.d.ts.map +1 -0
  14. package/dist/color/dimmer.js +16 -0
  15. package/dist/color/dimmer.js.map +1 -0
  16. package/dist/color/hsvcct.d.ts +3 -0
  17. package/dist/color/hsvcct.d.ts.map +1 -0
  18. package/dist/color/hsvcct.js +52 -0
  19. package/dist/color/hsvcct.js.map +1 -0
  20. package/dist/color/index.d.ts +7 -0
  21. package/dist/color/index.d.ts.map +1 -0
  22. package/dist/color/index.js +28 -0
  23. package/dist/color/index.js.map +1 -0
  24. package/dist/color/parsers.d.ts +20 -0
  25. package/dist/color/parsers.d.ts.map +1 -0
  26. package/dist/color/parsers.js +232 -0
  27. package/dist/color/parsers.js.map +1 -0
  28. package/dist/color/rgb.d.ts +3 -0
  29. package/dist/color/rgb.d.ts.map +1 -0
  30. package/dist/color/rgb.js +32 -0
  31. package/dist/color/rgb.js.map +1 -0
  32. package/dist/color/rgbaw.d.ts +3 -0
  33. package/dist/color/rgbaw.d.ts.map +1 -0
  34. package/dist/color/rgbaw.js +48 -0
  35. package/dist/color/rgbaw.js.map +1 -0
  36. package/dist/color/rgbw.d.ts +3 -0
  37. package/dist/color/rgbw.d.ts.map +1 -0
  38. package/dist/color/rgbw.js +37 -0
  39. package/dist/color/rgbw.js.map +1 -0
  40. package/dist/color/rgbww.d.ts +3 -0
  41. package/dist/color/rgbww.d.ts.map +1 -0
  42. package/dist/color/rgbww.js +54 -0
  43. package/dist/color/rgbww.js.map +1 -0
  44. package/dist/color/types.d.ts +64 -0
  45. package/dist/color/types.d.ts.map +1 -0
  46. package/dist/color/types.js +91 -0
  47. package/dist/color/types.js.map +1 -0
  48. package/dist/config.d.ts +60 -0
  49. package/dist/config.d.ts.map +1 -0
  50. package/dist/config.js +191 -0
  51. package/dist/config.js.map +1 -0
  52. package/dist/controller.d.ts +32 -0
  53. package/dist/controller.d.ts.map +1 -0
  54. package/dist/controller.js +128 -0
  55. package/dist/controller.js.map +1 -0
  56. package/dist/index.d.ts +4 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +7 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/platform.d.ts +17 -0
  61. package/dist/platform.d.ts.map +1 -0
  62. package/dist/platform.js +96 -0
  63. package/dist/platform.js.map +1 -0
  64. package/dist/platformAccessory.d.ts +18 -0
  65. package/dist/platformAccessory.d.ts.map +1 -0
  66. package/dist/platformAccessory.js +81 -0
  67. package/dist/platformAccessory.js.map +1 -0
  68. package/dist/settings.d.ts +14 -0
  69. package/dist/settings.d.ts.map +1 -0
  70. package/dist/settings.js +15 -0
  71. package/dist/settings.js.map +1 -0
  72. package/dist/stick/frame.d.ts +16 -0
  73. package/dist/stick/frame.d.ts.map +1 -0
  74. package/dist/stick/frame.js +51 -0
  75. package/dist/stick/frame.js.map +1 -0
  76. package/dist/stick/kdf.d.ts +23 -0
  77. package/dist/stick/kdf.d.ts.map +1 -0
  78. package/dist/stick/kdf.js +90 -0
  79. package/dist/stick/kdf.js.map +1 -0
  80. package/dist/stick/protocol.d.ts +28 -0
  81. package/dist/stick/protocol.d.ts.map +1 -0
  82. package/dist/stick/protocol.js +78 -0
  83. package/dist/stick/protocol.js.map +1 -0
  84. package/dist/stick/session.d.ts +50 -0
  85. package/dist/stick/session.d.ts.map +1 -0
  86. package/dist/stick/session.js +301 -0
  87. package/dist/stick/session.js.map +1 -0
  88. package/examples/dmx.yaml +65 -0
  89. package/package.json +51 -0
  90. package/src/cli/stick.ts +198 -0
  91. package/src/color/cct.ts +40 -0
  92. package/src/color/dimmer.ts +24 -0
  93. package/src/color/hsvcct.ts +59 -0
  94. package/src/color/index.ts +34 -0
  95. package/src/color/parsers.ts +232 -0
  96. package/src/color/rgb.ts +33 -0
  97. package/src/color/rgbaw.ts +46 -0
  98. package/src/color/rgbw.ts +36 -0
  99. package/src/color/rgbww.ts +56 -0
  100. package/src/color/types.ts +150 -0
  101. package/src/config.ts +248 -0
  102. package/src/controller.ts +136 -0
  103. package/src/index.ts +9 -0
  104. package/src/platform.ts +118 -0
  105. package/src/platformAccessory.ts +102 -0
  106. package/src/settings.ts +18 -0
  107. package/src/stick/frame.ts +70 -0
  108. package/src/stick/kdf.ts +99 -0
  109. package/src/stick/protocol.ts +90 -0
  110. package/src/stick/session.ts +348 -0
  111. package/stick-de3.json +6 -0
  112. package/tools/aes-hook.c +1404 -0
  113. package/tools/analyze-pcap.mjs +143 -0
  114. package/tools/capture-aes-key.lldb +25 -0
  115. package/tools/capture-and-verify.sh +46 -0
  116. package/tools/capture-handshake.sh +59 -0
  117. package/tools/capture-send.sh +39 -0
  118. package/tools/crack-kdf.mjs +121 -0
  119. package/tools/crack-key.mjs +170 -0
  120. package/tools/decode-frame.mjs +110 -0
  121. package/tools/decrypt-011c.mjs +123 -0
  122. package/tools/decrypt-dmx.mjs +130 -0
  123. package/tools/decrypt-ssl2.ts +154 -0
  124. package/tools/derive-dmx-key.mjs +189 -0
  125. package/tools/diff-011c.mjs +127 -0
  126. package/tools/dump-pcap-summary.mjs +54 -0
  127. package/tools/dump-tcp-stream.mjs +123 -0
  128. package/tools/extract-011c.mjs +117 -0
  129. package/tools/extract-frame.mjs +78 -0
  130. package/tools/find-011c-key.mjs +140 -0
  131. package/tools/find-iv.mjs +153 -0
  132. package/tools/ghidra/README.md +136 -0
  133. package/tools/ghidra/__pycache__/dump-011c-callers.cpython-313.pyc +0 -0
  134. package/tools/ghidra/__pycache__/dump-011c-handler.cpython-313.pyc +0 -0
  135. package/tools/ghidra/__pycache__/dump-aes-candidates.cpython-313.pyc +0 -0
  136. package/tools/ghidra/__pycache__/dump-anchors.cpython-313.pyc +0 -0
  137. package/tools/ghidra/__pycache__/dump-auth2.cpython-313.pyc +0 -0
  138. package/tools/ghidra/__pycache__/dump-authenticate.cpython-313.pyc +0 -0
  139. package/tools/ghidra/__pycache__/dump-block-cipher.cpython-313.pyc +0 -0
  140. package/tools/ghidra/__pycache__/dump-cipher.cpython-313.pyc +0 -0
  141. package/tools/ghidra/__pycache__/dump-ctor-chain.cpython-313.pyc +0 -0
  142. package/tools/ghidra/__pycache__/dump-curve.cpython-313.pyc +0 -0
  143. package/tools/ghidra/__pycache__/dump-encompassing.cpython-313.pyc +0 -0
  144. package/tools/ghidra/__pycache__/dump-handshake.cpython-313.pyc +0 -0
  145. package/tools/ghidra/__pycache__/dump-kdf-chain.cpython-313.pyc +0 -0
  146. package/tools/ghidra/__pycache__/dump-kdf-context.cpython-313.pyc +0 -0
  147. package/tools/ghidra/__pycache__/dump-profile.cpython-313.pyc +0 -0
  148. package/tools/ghidra/__pycache__/dump-real-encrypt.cpython-313.pyc +0 -0
  149. package/tools/ghidra/__pycache__/dump-secret.cpython-313.pyc +0 -0
  150. package/tools/ghidra/__pycache__/dump-setsoftware.cpython-313.pyc +0 -0
  151. package/tools/ghidra/__pycache__/dump-stick3-vtable.cpython-313.pyc +0 -0
  152. package/tools/ghidra/__pycache__/dump-swxref.cpython-313.pyc +0 -0
  153. package/tools/ghidra/__pycache__/dump-vt-key-getter.cpython-313.pyc +0 -0
  154. package/tools/ghidra/__pycache__/find-cipher.cpython-313.pyc +0 -0
  155. package/tools/ghidra/__pycache__/find-ctor.cpython-313.pyc +0 -0
  156. package/tools/ghidra/__pycache__/find-md5.cpython-313.pyc +0 -0
  157. package/tools/ghidra/__pycache__/find-secret-users.cpython-313.pyc +0 -0
  158. package/tools/ghidra/__pycache__/find-stick3-crypto.cpython-313.pyc +0 -0
  159. package/tools/ghidra/__pycache__/find-stick3-device.cpython-313.pyc +0 -0
  160. package/tools/ghidra/__pycache__/find-stick3a-ctor.cpython-313.pyc +0 -0
  161. package/tools/ghidra/__pycache__/find-stick3anet-ctor.cpython-313.pyc +0 -0
  162. package/tools/ghidra/__pycache__/find-vtables.cpython-313.pyc +0 -0
  163. package/tools/ghidra/__pycache__/string-walk.cpython-313.pyc +0 -0
  164. package/tools/ghidra/dump-011c-callers.py +140 -0
  165. package/tools/ghidra/dump-011c-handler.py +158 -0
  166. package/tools/ghidra/dump-aes-candidates.py +99 -0
  167. package/tools/ghidra/dump-anchors.py +192 -0
  168. package/tools/ghidra/dump-auth2.py +50 -0
  169. package/tools/ghidra/dump-authenticate.py +71 -0
  170. package/tools/ghidra/dump-block-cipher.py +88 -0
  171. package/tools/ghidra/dump-cipher.py +106 -0
  172. package/tools/ghidra/dump-ctor-chain.py +107 -0
  173. package/tools/ghidra/dump-curve.py +112 -0
  174. package/tools/ghidra/dump-encompassing.py +146 -0
  175. package/tools/ghidra/dump-handshake.py +76 -0
  176. package/tools/ghidra/dump-kdf-chain.py +105 -0
  177. package/tools/ghidra/dump-kdf-context.py +72 -0
  178. package/tools/ghidra/dump-profile.py +59 -0
  179. package/tools/ghidra/dump-real-encrypt.py +90 -0
  180. package/tools/ghidra/dump-secret.py +84 -0
  181. package/tools/ghidra/dump-setsoftware.py +63 -0
  182. package/tools/ghidra/dump-stick3-vtable.py +182 -0
  183. package/tools/ghidra/dump-swxref.py +27 -0
  184. package/tools/ghidra/dump-vt-key-getter.py +170 -0
  185. package/tools/ghidra/find-cipher.py +210 -0
  186. package/tools/ghidra/find-ctor.py +109 -0
  187. package/tools/ghidra/find-md5.py +97 -0
  188. package/tools/ghidra/find-secret-users.py +98 -0
  189. package/tools/ghidra/find-stick3-crypto.py +190 -0
  190. package/tools/ghidra/find-stick3-device.py +111 -0
  191. package/tools/ghidra/find-stick3a-ctor.py +87 -0
  192. package/tools/ghidra/find-stick3anet-ctor.py +76 -0
  193. package/tools/ghidra/find-vtables.py +165 -0
  194. package/tools/ghidra/out-011c-callers-run.log +51 -0
  195. package/tools/ghidra/out-011c-run.log +50 -0
  196. package/tools/ghidra/out-vt-key-run.log +55 -0
  197. package/tools/ghidra/string-walk.py +180 -0
  198. package/tools/hmac-key.sh +51 -0
  199. package/tools/hwm-entitlements.plist +18 -0
  200. package/tools/inspect-plaintext.mjs +87 -0
  201. package/tools/lldb-capture-aes.py +137 -0
  202. package/tools/lldb_hmac_key.py +93 -0
  203. package/tools/lldb_pathA_scan.py +341 -0
  204. package/tools/lldb_pathC_keytrace.py +300 -0
  205. package/tools/lldb_pathD_kdf_watch.py +281 -0
  206. package/tools/pathA-launch-hwm.sh +86 -0
  207. package/tools/pathA-scan.sh +52 -0
  208. package/tools/pathC-keytrace.sh +49 -0
  209. package/tools/pathC-run.sh +71 -0
  210. package/tools/pathD-kdf-watch.sh +51 -0
  211. package/tools/quick-trigger.mjs +86 -0
  212. package/tools/run-hooked-hwmanager.sh +51 -0
  213. package/tools/scan-aes-schedule.mjs +135 -0
  214. package/tools/send_dmx.mjs +480 -0
  215. package/tools/stick-status.mjs +57 -0
  216. package/tools/test-aes-detector.mjs +100 -0
  217. package/tools/test-tcp-write.ts +274 -0
  218. package/tools/test-udp-dmx.ts +324 -0
  219. package/tools/test-x25519.mjs +87 -0
  220. package/tools/try-decrypt.ts +144 -0
  221. package/tools/try-hardcoded-key.mjs +86 -0
  222. package/tools/try-hardcoded-key.ts +103 -0
  223. package/tools/try-key.mjs +103 -0
  224. package/tools/verify-011c-key.mjs +121 -0
  225. package/tools/verify-kdf.mjs +142 -0
  226. package/tsconfig.json +19 -0
@@ -0,0 +1,324 @@
1
+ import * as net from 'net';
2
+ import * as dgram from 'dgram';
3
+ import * as crypto from 'crypto';
4
+
5
+ const STICK_HOST = '192.168.96.2';
6
+ const STICK_PORT = 2431;
7
+ const MAGIC_CLIENT = Buffer.from('LSAG_ALL');
8
+ const MAGIC_STICK = Buffer.from('Stick_3A');
9
+
10
+ function hexDump(label: string, buf: Buffer, maxLen = 128) {
11
+ console.log(`\n--- ${label} (${buf.length} bytes) ---`);
12
+ const len = Math.min(buf.length, maxLen);
13
+ for (let i = 0; i < len; i += 16) {
14
+ const hex = [];
15
+ for (let j = 0; j < 16; j++) {
16
+ if (i + j < len) hex.push(buf[i + j].toString(16).padStart(2, '0'));
17
+ else hex.push(' ');
18
+ }
19
+ console.log(` ${i.toString(16).padStart(4, '0')} ${hex.join(' ')}`);
20
+ }
21
+ }
22
+
23
+ class StickConnection {
24
+ private sock: net.Socket;
25
+ private recvBuf = Buffer.alloc(0);
26
+ private resolveRead: ((buf: Buffer) => void) | null = null;
27
+ private readTarget = 0;
28
+ buffered = 0;
29
+
30
+ constructor() {
31
+ this.sock = new net.Socket();
32
+ this.sock.on('data', (data) => {
33
+ this.recvBuf = Buffer.concat([this.recvBuf, data]);
34
+ this.buffered = this.recvBuf.length;
35
+ this.checkRead();
36
+ });
37
+ }
38
+
39
+ private checkRead() {
40
+ if (this.resolveRead && this.recvBuf.length >= this.readTarget) {
41
+ const result = this.recvBuf.subarray(0, this.readTarget);
42
+ this.recvBuf = this.recvBuf.subarray(this.readTarget);
43
+ this.buffered = this.recvBuf.length;
44
+ const resolve = this.resolveRead;
45
+ this.resolveRead = null;
46
+ resolve(result);
47
+ }
48
+ }
49
+
50
+ read(n: number, timeout = 5000): Promise<Buffer> {
51
+ if (this.recvBuf.length >= n) {
52
+ const result = this.recvBuf.subarray(0, n);
53
+ this.recvBuf = this.recvBuf.subarray(n);
54
+ this.buffered = this.recvBuf.length;
55
+ return Promise.resolve(result);
56
+ }
57
+ return new Promise((resolve, reject) => {
58
+ this.readTarget = n;
59
+ this.resolveRead = resolve;
60
+ const timer = setTimeout(() => {
61
+ this.resolveRead = null;
62
+ reject(new Error(`timeout reading ${n} bytes, have ${this.recvBuf.length}`));
63
+ }, timeout);
64
+ this.resolveRead = (buf) => {
65
+ clearTimeout(timer);
66
+ resolve(buf);
67
+ };
68
+ });
69
+ }
70
+
71
+ async readMessage(): Promise<{ opcode: number; seq: number; payload: Buffer }> {
72
+ const header = await this.read(10);
73
+ const opcode = header.readUInt8(8) | (header.readUInt8(9) << 8);
74
+ if (opcode === 0xc9) {
75
+ const data = await this.read(48);
76
+ return { opcode, seq: 0, payload: data };
77
+ } else if (opcode === 0x25) {
78
+ const data = await this.read(30);
79
+ return { opcode, seq: 0, payload: data };
80
+ } else if (opcode === 0x47) {
81
+ const data = await this.read(44);
82
+ return { opcode, seq: data.readUInt16LE(0), payload: data.subarray(8) };
83
+ } else if (opcode === 0x48) {
84
+ const data = await this.read(12);
85
+ return { opcode, seq: data.readUInt16LE(0), payload: data.subarray(8) };
86
+ } else if (opcode === 0x011c) {
87
+ const ack = await this.read(12);
88
+ const payloadLen = ack.readUInt16LE(8);
89
+ const payload = await this.read(payloadLen);
90
+ return { opcode, seq: ack.readUInt16LE(0), payload };
91
+ } else {
92
+ const data = await this.read(8);
93
+ return { opcode, seq: data.readUInt16LE(0), payload: data.subarray(6) };
94
+ }
95
+ }
96
+
97
+ connect(): Promise<void> {
98
+ return new Promise((resolve, reject) => {
99
+ this.sock.connect(STICK_PORT, STICK_HOST, resolve);
100
+ this.sock.once('error', reject);
101
+ });
102
+ }
103
+
104
+ send(data: Buffer) { this.sock.write(data); }
105
+ close() { this.sock.destroy(); }
106
+ }
107
+
108
+ function buildPacket(magic: Buffer, opcode: number, seq: number, payload?: Buffer): Buffer {
109
+ const header = Buffer.alloc(18);
110
+ magic.copy(header, 0);
111
+ header.writeUInt16LE(opcode, 8);
112
+ header.writeUInt16LE(seq, 10);
113
+ if (payload) return Buffer.concat([header, payload]);
114
+ return header;
115
+ }
116
+
117
+ async function doHandshake(conn: StickConnection): Promise<Buffer> {
118
+ // Hello
119
+ const helloPacket = buildPacket(MAGIC_CLIENT, 0x47, 1);
120
+ conn.send(helloPacket);
121
+
122
+ let helloResp = await conn.readMessage();
123
+ while (helloResp.opcode === 0x25) {
124
+ helloResp = await conn.readMessage();
125
+ }
126
+ if (helloResp.opcode !== 0x47) throw new Error(`Expected 0x47, got 0x${helloResp.opcode.toString(16)}`);
127
+
128
+ const stickPubKey = helloResp.payload.subarray(4, 36);
129
+ console.log(`Stick pubkey: ${stickPubKey.toString('hex')}`);
130
+
131
+ // ECDH
132
+ const clientKeys = crypto.generateKeyPairSync('x25519');
133
+ const clientPubRaw = clientKeys.publicKey.export({ type: 'spki', format: 'der' });
134
+ const clientPubKey = clientPubRaw.subarray(clientPubRaw.length - 32);
135
+
136
+ const stickPubKeyObj = crypto.createPublicKey({
137
+ key: Buffer.concat([Buffer.from('302a300506032b656e032100', 'hex'), stickPubKey]),
138
+ format: 'der',
139
+ type: 'spki',
140
+ });
141
+ const sharedSecret = crypto.diffieHellman({
142
+ privateKey: clientKeys.privateKey,
143
+ publicKey: stickPubKeyObj,
144
+ });
145
+ console.log(`Shared secret: ${sharedSecret.toString('hex')}`);
146
+
147
+ // Key exchange
148
+ const softwareName = Buffer.alloc(32);
149
+ Buffer.from('software').copy(softwareName);
150
+ const keyExPayload = Buffer.concat([softwareName, stickPubKey, clientPubKey]);
151
+ conn.send(buildPacket(MAGIC_CLIENT, 0x48, 7, keyExPayload));
152
+ const keyExResp = await conn.readMessage();
153
+ console.log(`Key exchange: opcode=0x${keyExResp.opcode.toString(16)}`);
154
+
155
+ // Init
156
+ conn.send(Buffer.from([...MAGIC_STICK, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00]));
157
+
158
+ // Device query
159
+ conn.send(Buffer.from([...MAGIC_STICK, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00]));
160
+ const devResp = await conn.readMessage();
161
+ console.log(`Device: opcode=0x${devResp.opcode.toString(16)}`);
162
+
163
+ // Session setup
164
+ conn.send(buildPacket(MAGIC_STICK, 0x011c, 8, Buffer.from([0x15, 0x00, 0x16, 0x00])));
165
+ const sessResp = await conn.readMessage();
166
+ console.log(`Session: opcode=0x${sessResp.opcode.toString(16)}`);
167
+
168
+ // Drain extras
169
+ await new Promise(r => setTimeout(r, 500));
170
+ while (conn.buffered > 0) {
171
+ await conn.readMessage().catch(() => null);
172
+ }
173
+
174
+ return sharedSecret;
175
+ }
176
+
177
+ function buildDmxFrame(
178
+ seq: number,
179
+ counter: number,
180
+ dmx: Buffer, // 512-byte DMX universe
181
+ key: Buffer, // 32-byte AES-256 key
182
+ ivStrategy: string,
183
+ ): Buffer {
184
+ const frame = Buffer.alloc(576);
185
+
186
+ // Header (bytes 0-31, plaintext)
187
+ MAGIC_STICK.copy(frame, 0); // 0-7: "Stick_3A"
188
+ frame.writeUInt16LE(0x19, 8); // 8-9: opcode
189
+ frame.writeUInt16LE(seq, 10); // 10-11: sequence
190
+ // 12-17: zeros (reserved)
191
+ frame[18] = 0x00;
192
+ frame[19] = 0x00;
193
+ frame[20] = 0x00;
194
+ frame[21] = 0x02;
195
+ frame[22] = 0x64; // 0x64 = 100
196
+ frame[23] = counter & 0xFF; // incrementing counter
197
+
198
+ // Plaintext for encryption (bytes 32-575 = 544 bytes)
199
+ // First 32 bytes: header/metadata within encrypted region
200
+ // Then 512 bytes: DMX universe
201
+ const plaintext = Buffer.alloc(544);
202
+ // The first 16 bytes of plaintext might be a timestamp or sequence info
203
+ // Try different layouts
204
+ dmx.copy(plaintext, 32); // DMX data at offset 32 within encrypted region
205
+
206
+ // Generate IV based on strategy
207
+ let iv: Buffer;
208
+ switch (ivStrategy) {
209
+ case 'zeros':
210
+ iv = Buffer.alloc(16);
211
+ break;
212
+ case 'header':
213
+ // Use bytes 8-23 of the frame header as IV
214
+ iv = frame.subarray(8, 24);
215
+ break;
216
+ case 'counter':
217
+ // IV = sequence number padded to 16 bytes
218
+ iv = Buffer.alloc(16);
219
+ iv.writeUInt16LE(seq, 0);
220
+ break;
221
+ case 'counter-be':
222
+ iv = Buffer.alloc(16);
223
+ iv.writeUInt32BE(seq, 12);
224
+ break;
225
+ case 'bytes24-31':
226
+ // Maybe bytes 24-39 of the frame are the IV (not encrypted, just overwritten)
227
+ iv = Buffer.alloc(16);
228
+ break;
229
+ default:
230
+ iv = Buffer.alloc(16);
231
+ }
232
+
233
+ // AES-128-CBC encrypt (16-byte key)
234
+ const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
235
+ cipher.setAutoPadding(false); // No padding, exact 544 bytes
236
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
237
+ encrypted.copy(frame, 32);
238
+
239
+ return frame;
240
+ }
241
+
242
+ const SERIAL = '685678';
243
+ const PASSWORD = '%$uy}g!B';
244
+
245
+ // AES-128 → 16-byte keys
246
+ const sha16 = (b: Buffer) => crypto.createHash('sha256').update(b).digest().subarray(0, 16);
247
+ const md5b = (b: Buffer) => crypto.createHash('md5').update(b).digest();
248
+ const S = Buffer.from(SERIAL);
249
+ const P = Buffer.from(PASSWORD);
250
+
251
+ function deriveKeys(sharedSecret: Buffer): { name: string; key: Buffer }[] {
252
+ return [
253
+ { name: 'shared[:16]', key: sharedSecret.subarray(0, 16) },
254
+ { name: 'md5(shared)', key: md5b(sharedSecret) },
255
+ { name: 'sha16(shared)', key: sha16(sharedSecret) },
256
+ { name: 'md5(shared+serial)', key: md5b(Buffer.concat([sharedSecret, S])) },
257
+ { name: 'md5(shared+pwd)', key: md5b(Buffer.concat([sharedSecret, P])) },
258
+ { name: 'md5(shared+serial+pwd)', key: md5b(Buffer.concat([sharedSecret, S, P])) },
259
+ { name: 'md5(shared+pwd+serial)', key: md5b(Buffer.concat([sharedSecret, P, S])) },
260
+ { name: 'md5(serial+shared)', key: md5b(Buffer.concat([S, sharedSecret])) },
261
+ { name: 'md5(pwd+shared)', key: md5b(Buffer.concat([P, sharedSecret])) },
262
+ { name: 'sha16(shared+serial)', key: sha16(Buffer.concat([sharedSecret, S])) },
263
+ { name: 'sha16(shared+pwd)', key: sha16(Buffer.concat([sharedSecret, P])) },
264
+ { name: 'sha16(shared+pwd+serial)', key: sha16(Buffer.concat([sharedSecret, P, S])) },
265
+ { name: 'hmac-md5(pwd,shared)', key: crypto.createHmac('md5', P).update(sharedSecret).digest() },
266
+ { name: 'hmac-md5(shared,pwd)', key: crypto.createHmac('md5', sharedSecret).update(P).digest() },
267
+ { name: 'hmac-sha256(pwd,shared)[:16]', key: crypto.createHmac('sha256', P).update(sharedSecret).digest().subarray(0, 16) },
268
+ { name: 'shared_xor_md5pwd', key: (() => {
269
+ const h = md5b(P);
270
+ const r = Buffer.alloc(16);
271
+ for (let i = 0; i < 16; i++) r[i] = sharedSecret[i] ^ h[i];
272
+ return r;
273
+ })() },
274
+ ];
275
+ }
276
+
277
+ async function main() {
278
+ const args = process.argv.slice(2);
279
+ const keyName = args[0] || 'all';
280
+ const ivStrategy = args[1] || 'zeros';
281
+
282
+ console.log('Connecting to Stick...');
283
+
284
+ const conn = new StickConnection();
285
+ await conn.connect();
286
+ console.log('Connected. Doing handshake...');
287
+
288
+ const sharedSecret = await doHandshake(conn);
289
+ console.log(`Shared secret: ${sharedSecret.toString('hex')}`);
290
+
291
+ const allKeys = deriveKeys(sharedSecret);
292
+
293
+ const dmx = Buffer.alloc(512);
294
+ dmx[5] = 255; // ch6 = intensity
295
+ dmx[8] = 255; // ch9 = saturation
296
+ dmx[9] = 85; // ch10 = hue (green)
297
+
298
+ const udp = dgram.createSocket('udp4');
299
+
300
+ const keysToTry = keyName === 'all' ? allKeys : allKeys.filter(k => k.name === keyName);
301
+
302
+ for (const { name, key } of keysToTry) {
303
+ console.log(`\n--- Trying key: ${name} (iv=${ivStrategy}) ---`);
304
+ console.log(` key: ${key.toString('hex')}`);
305
+
306
+ let seq = 0;
307
+ let counter = 0;
308
+ for (let i = 0; i < 200; i++) { // ~5 seconds worth at 40fps
309
+ const frame = buildDmxFrame(seq, counter, dmx, key, ivStrategy);
310
+ udp.send(frame, 0, 576, STICK_PORT, STICK_HOST);
311
+ seq = (seq + 4) & 0xFFFF;
312
+ counter = (counter + 1) & 0xFF;
313
+ await new Promise(r => setTimeout(r, 25));
314
+ }
315
+ console.log(` Sent 200 frames. Check light for GREEN.`);
316
+ await new Promise(r => setTimeout(r, 2000));
317
+ }
318
+
319
+ console.log('\nAll keys tried.');
320
+ udp.close();
321
+ conn.close();
322
+ }
323
+
324
+ main().catch(console.error);
@@ -0,0 +1,87 @@
1
+ // Test the hypothesis: the hardcoded 33-byte secret is the client's static
2
+ // X25519 private key (32 bytes + 1 flag/version byte). If so, the client
3
+ // public key sent in msg#3 should equal X25519(priv, base_point).
4
+
5
+ import * as crypto from 'node:crypto';
6
+
7
+ const HARDCODED = Buffer.from('527a5b46c56f3a5e670b4f0e338727d94737ec0fc4af0dba93a51d93965191d08f', 'hex');
8
+
9
+ // From captured handshake msg#2 (Stick→Client, opcode 0x47, bytes +0x16..+0x36)
10
+ const STICK_PUB = Buffer.from('966cb0a9667596d27c12abd2fe36ba95e390558bd9d072d5158cf7d7e1cea6d2', 'hex');
11
+
12
+ // From captured handshake msg#3 (Client→Stick, opcode 0x48, bytes +0x52..+0x72)
13
+ const CLIENT_PUB = Buffer.from('973a20b248501227162a0ad004836a015d76ade1c2186e20965ccb3d259d7ba0', 'hex');
14
+
15
+ console.log('HARDCODED (33B):');
16
+ console.log(' ', HARDCODED.toString('hex'));
17
+ console.log(' first 32B (candidate priv key):', HARDCODED.subarray(0, 32).toString('hex'));
18
+ console.log(' byte 33 (flag?): 0x' + HARDCODED[32].toString(16));
19
+ console.log();
20
+ console.log('STICK pubkey from msg#2: ', STICK_PUB.toString('hex'));
21
+ console.log('CLIENT pubkey from msg#3: ', CLIENT_PUB.toString('hex'));
22
+ console.log();
23
+
24
+ // Test 1: derive pubkey from hardcoded priv using X25519
25
+ // Node's crypto module: X25519 keys must be PKCS8 DER. Manual key gen.
26
+ function derivePub_x25519(priv32) {
27
+ // Node 16+ supports X25519 via crypto.createPrivateKey with raw key import.
28
+ const privKey = crypto.createPrivateKey({
29
+ key: Buffer.concat([
30
+ Buffer.from('302e020100300506032b656e04220420', 'hex'), // PKCS8 X25519 header
31
+ priv32,
32
+ ]),
33
+ format: 'der',
34
+ type: 'pkcs8',
35
+ });
36
+ // Export public via key derivation
37
+ const pubKey = crypto.createPublicKey(privKey);
38
+ const der = pubKey.export({ format: 'der', type: 'spki' });
39
+ // Last 32 bytes of SPKI DER = raw public key
40
+ return der.subarray(der.length - 32);
41
+ }
42
+
43
+ console.log('=== Test 1: X25519 — derive pub from hardcoded priv ===');
44
+ try {
45
+ const pub = derivePub_x25519(HARDCODED.subarray(0, 32));
46
+ console.log(' derived pub: ', pub.toString('hex'));
47
+ console.log(' client pub: ', CLIENT_PUB.toString('hex'));
48
+ console.log(' MATCH? ', pub.equals(CLIENT_PUB) ? '✓ YES — X25519 confirmed' : '✗ NO');
49
+ if (pub.equals(CLIENT_PUB)) {
50
+ // Compute shared secret
51
+ const sharedKey = crypto.diffieHellman({
52
+ privateKey: crypto.createPrivateKey({
53
+ key: Buffer.concat([Buffer.from('302e020100300506032b656e04220420','hex'), HARDCODED.subarray(0,32)]),
54
+ format: 'der', type: 'pkcs8',
55
+ }),
56
+ publicKey: crypto.createPublicKey({
57
+ key: Buffer.concat([Buffer.from('302a300506032b656e032100','hex'), STICK_PUB]),
58
+ format: 'der', type: 'spki',
59
+ }),
60
+ });
61
+ console.log('\n=== SHARED SECRET (X25519 ECDH) ===');
62
+ console.log(' shared: ', sharedKey.toString('hex'));
63
+ console.log(' SHA-256(shared)[:16] (candidate AES key):',
64
+ crypto.createHash('sha256').update(sharedKey).digest().subarray(0, 16).toString('hex'));
65
+ console.log(' raw shared[:16] (candidate AES key): ', sharedKey.subarray(0, 16).toString('hex'));
66
+ }
67
+ } catch (e) {
68
+ console.log(' error:', e.message);
69
+ }
70
+
71
+ console.log();
72
+ console.log('=== Test 2: same with reverse byte order ===');
73
+ try {
74
+ const priv = Buffer.from(HARDCODED.subarray(0, 32)).reverse();
75
+ const pub = derivePub_x25519(priv);
76
+ console.log(' derived pub: ', pub.toString('hex'));
77
+ console.log(' match client?: ', pub.equals(CLIENT_PUB) ? '✓ YES' : '✗ no');
78
+ } catch (e) {
79
+ console.log(' error:', e.message);
80
+ }
81
+
82
+ console.log();
83
+ console.log('If Test 1 matches, the static-RE hardcoded secret IS the client priv key.');
84
+ console.log('From there, every session: shared = X25519(hardcoded_priv, stick_pub_from_msg2),');
85
+ console.log('then AES key = some_hash(shared) [TBD: SHA-256[:16], HKDF, raw[:16]…].');
86
+ console.log("If neither matches, ECC is on a different curve (P-256, Curve448) or priv");
87
+ console.log("isn't the hardcoded secret.");
@@ -0,0 +1,144 @@
1
+ import * as crypto from 'crypto';
2
+
3
+ const frameHex =
4
+ '53746963 6b5f3341 19009600 00000000' +
5
+ '00000000 00026400 a683d3d3 ca57966b' +
6
+ '9eff43dc 8db5464f c0afc785 5bb424ad' +
7
+ 'fdb6a0ee 24df6433 cc952097 e1784c09' +
8
+ 'cd3c7b8b 83d5bdd3 ba136145 d4ff845a' +
9
+ '23bd8d28 d218a5df 4c784c8b caee7100' +
10
+ '0d586311 0e45051b edf184d4 23447235' +
11
+ '1ee2acbd 21eb8db0 76c54d53 f45b3706' +
12
+ '00445512 79ccf175 793483d9 98b6e1cb' +
13
+ '47733211 1e602f0e aa460c71 7bfc62b8' +
14
+ '9cc446b7 3db8a8bf 016de03b 8eed3e30' +
15
+ 'f9da8771 d263483e 4060b081 181edcd0' +
16
+ '4ebc3994 48d5b784 0d42355a 32851c65' +
17
+ '7ef9375f 841a93a1 8b3e3355 8c4f4cb6' +
18
+ 'a5d7824f ca89c958 97e787f0 ac0c7fcc' +
19
+ '4ee08dcb 0e10eb40 43f35441 6bca415d' +
20
+ '2a56502a 6c590b6b f7efe715 cba281ff' +
21
+ 'd0a123e7 c19d9fab 6a0f9d0c 285f3d58' +
22
+ 'ed34ac7d 0b555d78 9027f807 7b311b6a' +
23
+ '9f0d4f1c 485c1742 84dc0c32 48e370d6' +
24
+ '35cae307 bc5a7f9a f46132fc 15116b2a' +
25
+ '9ef095bd b18d244a b15d9b3f 599250cd' +
26
+ '72afb805 e987b4a0 45f8d417 2b3590a3' +
27
+ '6b594f4a ed96fd0f e1bf3501 d1fb9df9' +
28
+ '119a35a3 8195a827 0f064df5 1f629e0c' +
29
+ 'cb8ec788 28b31b78 005dd41a e40520b8' +
30
+ '5905a399 7dbb18ba 49372652 7b34fba4' +
31
+ 'fd206b5d a6dab5ab e3ef6cb5 efbd4de8' +
32
+ '3cbd6439 d41b45cf 77a5158a 901b68f6' +
33
+ '99a2a8cb 7919b848 a5dd0b4d 40cbe049' +
34
+ '6e500d65 ae1bc5d2 4fcf5b14 dbfc8f3d' +
35
+ '44ceb25a 4f6a1685 ecc5467c 2d6c62c0' +
36
+ 'd4e079f2 25362b08 4f31924b e4eef009' +
37
+ '37f7dc2b c03f0358 b7207ded 659d6388' +
38
+ 'b3261cb1 0774cafb 214c63db 95e9ad41' +
39
+ '5ecb2455 7d54eb1c 9c03a27d 154e68b0';
40
+ const frame = Buffer.from(frameHex.replace(/ /g, ''), 'hex');
41
+ console.log(`Frame size: ${frame.length} bytes`);
42
+
43
+ const ciphertext = frame.subarray(32); // 544 bytes
44
+
45
+ const serial = '685678';
46
+ const password = '%$uy}g!B';
47
+
48
+ // AES-128 → all keys MUST be 16 bytes
49
+ const sha16 = (s: string | Buffer) => crypto.createHash('sha256').update(s).digest().subarray(0, 16);
50
+ const md5 = (s: string | Buffer) => crypto.createHash('md5').update(s).digest(); // 16 bytes
51
+ const pad16 = (s: string) => Buffer.concat([Buffer.from(s), Buffer.alloc(16)]).subarray(0, 16);
52
+
53
+ const keys: { name: string; key: Buffer }[] = [
54
+ { name: 'NIST128 test (00-0f)', key: Buffer.from('000102030405060708090a0b0c0d0e0f', 'hex') },
55
+ { name: 'MD5(password)', key: md5(password) },
56
+ { name: 'MD5(serial)', key: md5(serial) },
57
+ { name: 'MD5(serial+password)', key: md5(serial + password) },
58
+ { name: 'MD5(password+serial)', key: md5(password + serial) },
59
+ { name: 'MD5(serial+":"+password)', key: md5(serial + ':' + password) },
60
+ { name: 'MD5(password+":"+serial)', key: md5(password + ':' + serial) },
61
+ { name: 'SHA256(password)[:16]', key: sha16(password) },
62
+ { name: 'SHA256(serial)[:16]', key: sha16(serial) },
63
+ { name: 'SHA256(serial+password)[:16]', key: sha16(serial + password) },
64
+ { name: 'SHA256(password+serial)[:16]', key: sha16(password + serial) },
65
+ { name: 'password padded16', key: pad16(password) },
66
+ { name: 'serial padded16', key: pad16(serial) },
67
+ { name: 'password+serial padded16', key: pad16(password + serial) },
68
+ { name: 'MD5(serial_hex)', key: md5(Buffer.from('685678', 'hex')) },
69
+ { name: 'SHA256(serial_hex)[:16]', key: sha16(Buffer.from('685678', 'hex')) },
70
+ ];
71
+
72
+ // IV strategies
73
+ function tryDecrypt(key: Buffer, iv: Buffer, label: string) {
74
+ try {
75
+ const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
76
+ decipher.setAutoPadding(false);
77
+ const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
78
+
79
+ // Check if result looks like valid plaintext
80
+ // DMX data should be at offset 32 within plaintext (512 bytes)
81
+ // Most bytes should be 0 for an idle universe
82
+ const dmxRegion = plain.subarray(32);
83
+ let zeroCount = 0;
84
+ for (let i = 0; i < dmxRegion.length; i++) {
85
+ if (dmxRegion[i] === 0) zeroCount++;
86
+ }
87
+ const zeroPct = Math.round(100 * zeroCount / dmxRegion.length);
88
+
89
+ // Show first 64 bytes of plaintext
90
+ console.log(`\n${label}:`);
91
+ console.log(` zeros in DMX region: ${zeroCount}/${dmxRegion.length} (${zeroPct}%)`);
92
+ console.log(` first 64 bytes: ${plain.subarray(0, 64).toString('hex')}`);
93
+ if (zeroPct > 80) {
94
+ console.log(` *** LIKELY VALID! ***`);
95
+ console.log(` full plaintext:`);
96
+ for (let i = 0; i < plain.length; i += 32) {
97
+ console.log(` ${i.toString(16).padStart(4, '0')}: ${plain.subarray(i, Math.min(i + 32, plain.length)).toString('hex')}`);
98
+ }
99
+ }
100
+ } catch (e: any) {
101
+ console.log(` ${label}: ERROR ${e.message}`);
102
+ }
103
+ }
104
+
105
+ for (const { name, key } of keys) {
106
+ const headerBytes = frame.subarray(24, 32); // bytes 24-31 from frame
107
+
108
+ // Try various IVs
109
+ tryDecrypt(key, Buffer.alloc(16), `${name} + IV=zeros`);
110
+ tryDecrypt(key, Buffer.concat([headerBytes, Buffer.alloc(8)]), `${name} + IV=bytes24-31+zeros`);
111
+ tryDecrypt(key, Buffer.concat([Buffer.alloc(8), headerBytes]), `${name} + IV=zeros+bytes24-31`);
112
+
113
+ // IV from frame header fields
114
+ const ivFromHeader = Buffer.alloc(16);
115
+ frame.copy(ivFromHeader, 0, 8, 24); // bytes 8-23
116
+ tryDecrypt(key, ivFromHeader, `${name} + IV=bytes8-23`);
117
+
118
+ // IV = first 16 bytes of encrypted region (then decrypt remaining 528)
119
+ const iv16 = ciphertext.subarray(0, 16);
120
+ try {
121
+ const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv16);
122
+ decipher.setAutoPadding(false);
123
+ const plain = Buffer.concat([decipher.update(ciphertext.subarray(16)), decipher.final()]);
124
+ let zeroCount = 0;
125
+ for (let i = 32; i < plain.length; i++) if (plain[i] === 0) zeroCount++;
126
+ const zeroPct = Math.round(100 * zeroCount / (plain.length - 32));
127
+ console.log(`\n${name} + IV=first16cipher (decrypt remaining 528):`);
128
+ console.log(` zeros: ${zeroCount}/${plain.length - 32} (${zeroPct}%)`);
129
+ console.log(` first 64: ${plain.subarray(0, 64).toString('hex')}`);
130
+ if (zeroPct > 80) console.log(` *** LIKELY VALID! ***`);
131
+ } catch {}
132
+
133
+ // Counter-based IV
134
+ const seq = frame.readUInt16LE(10);
135
+ const ivSeq = Buffer.alloc(16);
136
+ ivSeq.writeUInt16LE(seq, 0);
137
+ tryDecrypt(key, ivSeq, `${name} + IV=seq(${seq})`);
138
+
139
+ ivSeq.fill(0);
140
+ ivSeq.writeUInt32BE(seq, 12);
141
+ tryDecrypt(key, ivSeq, `${name} + IV=seq_be(${seq})`);
142
+ }
143
+
144
+ console.log('\nDone.');
@@ -0,0 +1,86 @@
1
+ // Try decrypting a captured 576-byte DMX frame using the hardcoded 33-byte
2
+ // secret discovered by Ghidra static RE.
3
+ //
4
+ // See tools/try-hardcoded-key.ts (TS twin) for the full backstory. This .mjs
5
+ // version is plain Node so it runs without tsx.
6
+
7
+ import * as crypto from 'node:crypto';
8
+ import * as fs from 'node:fs';
9
+
10
+ const KEY = Buffer.from('527a5b46c56f3a5e670b4f0e338727d9', 'hex');
11
+ const IV = Buffer.from('4737ec0fc4af0dba93a51d93965191d0', 'hex');
12
+
13
+ if (process.argv.length < 3) {
14
+ console.error('usage: node tools/try-hardcoded-key.mjs <captured-576B-frame.bin>');
15
+ process.exit(1);
16
+ }
17
+
18
+ const frame = fs.readFileSync(process.argv[2]);
19
+ if (frame.length !== 576) {
20
+ console.error(`expected exactly 576 bytes, got ${frame.length}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ const header = frame.subarray(0, 0x20);
25
+ const body = frame.subarray(0x20, 0x240);
26
+
27
+ console.log('clear header (32B):');
28
+ console.log(' ', header.toString('hex').match(/../g).join(' '));
29
+ console.log(` session_magic[0..8] = ${header.subarray(0, 8).toString('hex')}`);
30
+ console.log(` opcode/ver[8..a] = 0x${header.readUInt16LE(8).toString(16)}`);
31
+ console.log(` field[a..12] = ${header.subarray(0x0a, 0x12).toString('hex')}`);
32
+ console.log(` field[12..14] = 0x${header.readUInt16LE(0x12).toString(16)}`);
33
+ console.log(` channel count[14..16] = ${header.readUInt16LE(0x14)}`);
34
+ console.log(` const100[16] = 0x${header[0x16].toString(16)}`);
35
+ console.log(` seq_counter[17] = ${header[0x17]}`);
36
+ console.log(` nonce[18..20] = ${header.subarray(0x18, 0x20).toString('hex')}`);
37
+ console.log();
38
+
39
+ function score(buf) {
40
+ let zeros = 0;
41
+ for (const b of buf) if (b === 0) zeros++;
42
+ return {
43
+ zeroRatio: zeros / buf.length,
44
+ sample: buf.subarray(0, 48).toString('hex'),
45
+ };
46
+ }
47
+
48
+ function tryDecrypt(label, algo, key, iv) {
49
+ try {
50
+ const d = crypto.createDecipheriv(algo, key, iv);
51
+ d.setAutoPadding(false);
52
+ const out = Buffer.concat([d.update(body), d.final()]);
53
+ const s = score(out);
54
+ console.log(`${label}:`);
55
+ console.log(` zero ratio: ${(s.zeroRatio * 100).toFixed(1)}% (DMX with mostly off lights should be ~95%)`);
56
+ console.log(` first 48B: ${s.sample}`);
57
+ } catch (e) {
58
+ console.log(`${label}: error: ${e.message}`);
59
+ }
60
+ }
61
+
62
+ console.log('=== Variant A: hardcoded key + hardcoded IV ===');
63
+ tryDecrypt('AES-128-CBC', 'aes-128-cbc', KEY, IV);
64
+ tryDecrypt('AES-128-CFB', 'aes-128-cfb', KEY, IV);
65
+
66
+ console.log();
67
+ console.log('=== Variant B: hardcoded key + nonce-as-IV (frame[+0x18..+0x20] zero-padded) ===');
68
+ const nonceIv = Buffer.concat([header.subarray(0x18, 0x20), Buffer.alloc(8)]);
69
+ tryDecrypt('AES-128-CBC', 'aes-128-cbc', KEY, nonceIv);
70
+ tryDecrypt('AES-128-CFB', 'aes-128-cfb', KEY, nonceIv);
71
+
72
+ console.log();
73
+ console.log('=== Variant C: hardcoded key + full clear-header[16..32] as IV ===');
74
+ const headerTailIv = header.subarray(0x10, 0x20);
75
+ tryDecrypt('AES-128-CBC', 'aes-128-cbc', KEY, headerTailIv);
76
+ tryDecrypt('AES-128-CFB', 'aes-128-cfb', KEY, headerTailIv);
77
+
78
+ console.log();
79
+ console.log('=== Variant D: hardcoded IV used as the key, hardcoded key used as IV ===');
80
+ tryDecrypt('AES-128-CBC', 'aes-128-cbc', IV, KEY);
81
+ tryDecrypt('AES-128-CFB', 'aes-128-cfb', IV, KEY);
82
+
83
+ console.log();
84
+ console.log('If any variant above shows zero-ratio > 85% + plausible DMX values,');
85
+ console.log('the hardcoded secret IS the key/IV. If none works, the handshake');
86
+ console.log("derives the key per-session (cipher's vt[4/5/6] = opcodes 0xF/0x10/0x11).");