@tikoci/centrs 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 (182) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/GLOSSARY.txt +420 -0
  3. package/README.md +196 -0
  4. package/SECURITY.md +30 -0
  5. package/commands/AGENTS.md +50 -0
  6. package/commands/btest/README.md +257 -0
  7. package/commands/btest/examples.md +190 -0
  8. package/commands/check/README.md +59 -0
  9. package/commands/devices/README.md +405 -0
  10. package/commands/devices/examples.md +488 -0
  11. package/commands/discover/README.md +170 -0
  12. package/commands/discover/examples.md +98 -0
  13. package/commands/execute/README.md +208 -0
  14. package/commands/execute/examples.md +353 -0
  15. package/commands/mcp/README.md +260 -0
  16. package/commands/mcp/examples.md +158 -0
  17. package/commands/retrieve/README.md +209 -0
  18. package/commands/retrieve/examples.md +306 -0
  19. package/commands/settings/README.md +69 -0
  20. package/commands/stream/README.md +70 -0
  21. package/commands/terminal/README.md +177 -0
  22. package/commands/terminal/examples.md +144 -0
  23. package/commands/transfer/README.md +315 -0
  24. package/commands/transfer/examples.md +368 -0
  25. package/docs/CONSTITUTION.md +417 -0
  26. package/docs/MATRIX.md +224 -0
  27. package/docs/errors/README.md +74 -0
  28. package/docs/errors/auth/failed.md +9 -0
  29. package/docs/errors/cdb/already-exists.md +9 -0
  30. package/docs/errors/cdb/created.md +9 -0
  31. package/docs/errors/cdb/decrypt-failed.md +9 -0
  32. package/docs/errors/cdb/empty-group.md +9 -0
  33. package/docs/errors/cdb/invalid-option.md +9 -0
  34. package/docs/errors/cdb/invalid-record.md +9 -0
  35. package/docs/errors/cdb/not-found-target.md +9 -0
  36. package/docs/errors/cdb/not-found.md +9 -0
  37. package/docs/errors/cdb/override-applied.md +9 -0
  38. package/docs/errors/cdb/parse-failed.md +9 -0
  39. package/docs/errors/cdb/password-not-needed.md +9 -0
  40. package/docs/errors/cdb/password-required.md +9 -0
  41. package/docs/errors/cdb/reserved-key.md +9 -0
  42. package/docs/errors/cdb/reserved-option.md +9 -0
  43. package/docs/errors/cdb/target-not-registered.md +9 -0
  44. package/docs/errors/cdb/unknown-field.md +9 -0
  45. package/docs/errors/cdb/unknown-option.md +9 -0
  46. package/docs/errors/cdb/write-not-permitted.md +9 -0
  47. package/docs/errors/discover/broadcast-unavailable.md +9 -0
  48. package/docs/errors/discover/failed.md +9 -0
  49. package/docs/errors/identity/ambiguous.md +9 -0
  50. package/docs/errors/identity/no-match.md +9 -0
  51. package/docs/errors/input/invalid-command.md +9 -0
  52. package/docs/errors/input/invalid-match.md +9 -0
  53. package/docs/errors/input/invalid-path.md +9 -0
  54. package/docs/errors/input/invalid-routeros-path.md +9 -0
  55. package/docs/errors/input/invalid-target.md +9 -0
  56. package/docs/errors/input/local-file-not-found.md +9 -0
  57. package/docs/errors/input/mac-address.md +9 -0
  58. package/docs/errors/input/max-results-exceeded.md +9 -0
  59. package/docs/errors/internal/devices-failed.md +9 -0
  60. package/docs/errors/internal/mcp-start.md +9 -0
  61. package/docs/errors/internal/mcp-tool.md +9 -0
  62. package/docs/errors/internal/native-api-length.md +9 -0
  63. package/docs/errors/internal/unhandled.md +9 -0
  64. package/docs/errors/internal/unreachable.md +9 -0
  65. package/docs/errors/mndp/encode-failed.md +9 -0
  66. package/docs/errors/mndp/listen-failed.md +9 -0
  67. package/docs/errors/mndp/malformed.md +9 -0
  68. package/docs/errors/routeros/api-fatal.md +9 -0
  69. package/docs/errors/routeros/api-protocol.md +9 -0
  70. package/docs/errors/routeros/api-trap.md +9 -0
  71. package/docs/errors/routeros/btest-protocol.md +12 -0
  72. package/docs/errors/routeros/btest-too-many-sessions.md +9 -0
  73. package/docs/errors/routeros/command-failed.md +14 -0
  74. package/docs/errors/routeros/ec-srp5-protocol.md +11 -0
  75. package/docs/errors/routeros/error.md +9 -0
  76. package/docs/errors/routeros/invalid-value.md +14 -0
  77. package/docs/errors/routeros/mac-telnet-error.md +9 -0
  78. package/docs/errors/routeros/mac-telnet-not-ready.md +9 -0
  79. package/docs/errors/routeros/mac-telnet-protocol.md +9 -0
  80. package/docs/errors/routeros/mac-telnet-unsupported-auth.md +9 -0
  81. package/docs/errors/routeros/protocol-not-implemented.md +9 -0
  82. package/docs/errors/routeros/request-failed.md +9 -0
  83. package/docs/errors/routeros/session-closed.md +13 -0
  84. package/docs/errors/routeros/unknown-attribute.md +13 -0
  85. package/docs/errors/routeros/unknown-path.md +15 -0
  86. package/docs/errors/routeros/unsupported-capability.md +9 -0
  87. package/docs/errors/settings/invalid-boolean.md +9 -0
  88. package/docs/errors/settings/invalid-format.md +9 -0
  89. package/docs/errors/settings/invalid-integer.md +9 -0
  90. package/docs/errors/settings/invalid-timeout.md +9 -0
  91. package/docs/errors/settings/invalid-via.md +9 -0
  92. package/docs/errors/settings/unsafe-protocol-blocked.md +9 -0
  93. package/docs/errors/target/mac-not-in-arp.md +14 -0
  94. package/docs/errors/target/mac-required.md +23 -0
  95. package/docs/errors/target/mac-unresolved.md +17 -0
  96. package/docs/errors/target/unresolved.md +9 -0
  97. package/docs/errors/transport/auth-failed.md +9 -0
  98. package/docs/errors/transport/auto-method.md +9 -0
  99. package/docs/errors/transport/capability-unsupported.md +22 -0
  100. package/docs/errors/transport/checksum-unavailable.md +9 -0
  101. package/docs/errors/transport/connection-closed.md +9 -0
  102. package/docs/errors/transport/connection-refused.md +9 -0
  103. package/docs/errors/transport/dns.md +9 -0
  104. package/docs/errors/transport/host-key-mismatch.md +9 -0
  105. package/docs/errors/transport/incomplete-transfer.md +9 -0
  106. package/docs/errors/transport/insecure-trust.md +9 -0
  107. package/docs/errors/transport/local-tool-missing.md +11 -0
  108. package/docs/errors/transport/network.md +9 -0
  109. package/docs/errors/transport/timeout.md +9 -0
  110. package/docs/errors/transport/tls-certificate.md +9 -0
  111. package/docs/errors/transport/unreachable.md +9 -0
  112. package/docs/errors/transport/unsupported-operation.md +9 -0
  113. package/docs/errors/usage/confirmation-required.md +9 -0
  114. package/docs/errors/usage/conflicting-flags.md +9 -0
  115. package/docs/errors/usage/invalid-concurrency.md +9 -0
  116. package/docs/errors/usage/missing-group.md +9 -0
  117. package/docs/errors/usage/not-implemented.md +9 -0
  118. package/docs/errors/usage/target-exists.md +9 -0
  119. package/docs/errors/usage/timeout-out-of-range.md +9 -0
  120. package/docs/errors/validation/not-implemented.md +9 -0
  121. package/docs/errors/validation/option.md +9 -0
  122. package/docs/errors/validation/syntax.md +9 -0
  123. package/docs/errors/validation/unknown-attribute.md +9 -0
  124. package/docs/errors/validation/unknown-path.md +9 -0
  125. package/package.json +96 -0
  126. package/src/AGENTS.md +7 -0
  127. package/src/btest.ts +906 -0
  128. package/src/cli/btest.ts +455 -0
  129. package/src/cli/common.ts +53 -0
  130. package/src/cli/devices.ts +527 -0
  131. package/src/cli/discover.ts +186 -0
  132. package/src/cli/execute.ts +333 -0
  133. package/src/cli/mcp.ts +109 -0
  134. package/src/cli/retrieve.ts +383 -0
  135. package/src/cli/terminal.ts +303 -0
  136. package/src/cli/transfer.ts +471 -0
  137. package/src/cli.ts +115 -0
  138. package/src/core/envelope.ts +221 -0
  139. package/src/core/error-catalog.ts +469 -0
  140. package/src/core/routeros-errors.ts +268 -0
  141. package/src/data/AGENTS.md +11 -0
  142. package/src/data/dude-db.ts +0 -0
  143. package/src/data/mndp-cache.ts +132 -0
  144. package/src/data/mndp.ts +405 -0
  145. package/src/data/winbox-cdb-write.ts +200 -0
  146. package/src/data/winbox-cdb.ts +913 -0
  147. package/src/devices-schema.ts +107 -0
  148. package/src/devices.ts +1717 -0
  149. package/src/discover.ts +802 -0
  150. package/src/errors.ts +189 -0
  151. package/src/execute.ts +1350 -0
  152. package/src/index.ts +353 -0
  153. package/src/mcp/config.ts +66 -0
  154. package/src/mcp/resources.ts +98 -0
  155. package/src/mcp/safety.ts +119 -0
  156. package/src/mcp/server.ts +225 -0
  157. package/src/mcp/tools.ts +657 -0
  158. package/src/mcp.ts +33 -0
  159. package/src/protocols/AGENTS.md +14 -0
  160. package/src/protocols/adapter.ts +866 -0
  161. package/src/protocols/btest-session.ts +1493 -0
  162. package/src/protocols/btest.ts +447 -0
  163. package/src/protocols/ec-srp5.ts +466 -0
  164. package/src/protocols/index.ts +118 -0
  165. package/src/protocols/mac-telnet-console.ts +655 -0
  166. package/src/protocols/mac-telnet.ts +1299 -0
  167. package/src/protocols/mtwei.ts +47 -0
  168. package/src/protocols/native-api.ts +796 -0
  169. package/src/protocols/sftp.ts +427 -0
  170. package/src/protocols/ssh.ts +297 -0
  171. package/src/resolver/cdb.ts +580 -0
  172. package/src/resolver/comment-kv.ts +360 -0
  173. package/src/resolver/index.ts +74 -0
  174. package/src/resolver/mac.ts +227 -0
  175. package/src/resolver/settings.ts +228 -0
  176. package/src/resolver/target.ts +442 -0
  177. package/src/retrieve-fanout.ts +533 -0
  178. package/src/retrieve.ts +1215 -0
  179. package/src/terminal.ts +444 -0
  180. package/src/transfer.ts +1554 -0
  181. package/src/tui.ts +22 -0
  182. package/src/webproxy.ts +16 -0
@@ -0,0 +1,466 @@
1
+ /**
2
+ * EC-SRP5 — MikroTik's custom EC-SRP zero-knowledge password proof.
3
+ *
4
+ * MikroTik uses one EC-SRP family across MAC-Telnet (MTWEI), WinBox (EC-SRP5),
5
+ * and the bandwidth test (btest): a Curve25519 group expressed in short
6
+ * Weierstrass form, 32-byte big-endian Montgomery-X + 1 parity-byte point
7
+ * encoding, identity `SHA256(salt ‖ SHA256("user:pass"))`, and proof
8
+ * `SHA256(j ‖ z)`. This module is the **shared, protocol-agnostic** core — curve
9
+ * constants, field/point math, point encoding, REDP1, keygen, the identity
10
+ * validator, and the client proof. Per-protocol *framing* (how the public keys
11
+ * and proof ride the wire) lives in the protocol module: `mtwei.ts` for
12
+ * MAC-Telnet, `btest.ts` for the bandwidth test.
13
+ *
14
+ * This is a from-scratch, dependency-free port (BigInt field/point math +
15
+ * node:crypto SHA-256) of the client side of Håkon Nessjøen's `mtwei.c`
16
+ * (`haakonnessjoen/MAC-Telnet`, © Yandex 2022), cross-checked against the WinBox
17
+ * EC-SRP5 reference (`Lab/winbox-terminal-protocol`) and `tikoci/m2ir`'s
18
+ * `winbox-ipc/ec-srp5-handshake.yaml` (derived from MarginResearch's PoC). The
19
+ * curve constants, point encoding, identity hash, and proof derivation match all
20
+ * three, and the proof is accepted by real RouterOS over the MAC-Telnet L2
21
+ * harness.
22
+ */
23
+
24
+ import { createHash, randomBytes } from "node:crypto";
25
+ import { CentrsError } from "../errors.ts";
26
+
27
+ /** Client/server public key length: 32-byte Montgomery X + 1 parity byte. */
28
+ export const EC_SRP5_PUBKEY_LEN = 33;
29
+ /** Authentication proof / validator length. */
30
+ export const EC_SRP5_VALIDATOR_LEN = 32;
31
+
32
+ // ── Curve constants (verbatim from mtwei.c `mtwei_init`) ────────────────────
33
+ // Expressed via `BigInt("0x…")` rather than `0x…n` literals: CodeQL's JS
34
+ // extractor wrongly infers hex BigInt literals as possibly-undefined, which makes
35
+ // every downstream `+`/`*`/`%` here trip "implicit operand conversion". The
36
+ // constructor form is byte-identical (same hex value) and is correctly modelled
37
+ // as `bigint`, so the false positives go away without touching the math.
38
+ /** Field prime: 2^255 − 19. */
39
+ const P = BigInt(
40
+ "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed",
41
+ );
42
+ /** Short-Weierstrass curve coefficient a. */
43
+ const A = BigInt(
44
+ "0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144",
45
+ );
46
+ /** Short-Weierstrass curve coefficient b. */
47
+ const B = BigInt(
48
+ "0x7b425ed097b425ed097b425ed097b425ed097b425ed097b4260b5e9c7710c864",
49
+ );
50
+ /** Group order. */
51
+ const N = BigInt(
52
+ "0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed",
53
+ );
54
+ /** Generator X (Weierstrass affine). */
55
+ const GX = BigInt(
56
+ "0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad245a",
57
+ );
58
+ /** Generator Y (Weierstrass affine). */
59
+ const GY = BigInt(
60
+ "0x5f51e65e475f794b1fe122d388b72eb36dc2b28192839e4dd6163a5d81312c14",
61
+ );
62
+ /** Weierstrass-X → Montgomery-X additive conversion (mtwei.c `w2m`). */
63
+ const W2M = BigInt(
64
+ "0x555555555555555555555555555555555555555555555555555555555552db9c",
65
+ );
66
+ /** Montgomery-X → Weierstrass-X additive conversion (mtwei.c `m2w`). */
67
+ const M2W = BigInt(
68
+ "0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2451",
69
+ );
70
+ /** sqrt(−1) mod P, for the p ≡ 5 (mod 8) square-root path. */
71
+ const SQRT_M1 = modPow(2n, (P - 1n) / 4n, P);
72
+
73
+ /** An affine point, or `null` for the point at infinity. */
74
+ export type Point = { x: bigint; y: bigint } | null;
75
+
76
+ const G: Point = { x: GX, y: GY };
77
+
78
+ // ── Field arithmetic (mod P) ────────────────────────────────────────────────
79
+
80
+ function mod(value: bigint, m: bigint): bigint {
81
+ const r = value % m;
82
+ return r < 0n ? r + m : r;
83
+ }
84
+
85
+ function modPow(base: bigint, exp: bigint, m: bigint): bigint {
86
+ let result = 1n;
87
+ let b = mod(base, m);
88
+ let e = exp;
89
+ while (e > 0n) {
90
+ if (e & 1n) result = (result * b) % m;
91
+ b = (b * b) % m;
92
+ e >>= 1n;
93
+ }
94
+ return result;
95
+ }
96
+
97
+ /** Modular inverse via Fermat's little theorem (P is prime). */
98
+ function modInv(value: bigint): bigint {
99
+ return modPow(value, P - 2n, P);
100
+ }
101
+
102
+ /**
103
+ * Square root mod P. P ≡ 5 (mod 8), so a candidate is `a^((P+3)/8)`, adjusted
104
+ * by sqrt(−1) when needed. Returns null if `a` is a non-residue.
105
+ */
106
+ function modSqrt(a: bigint): bigint | null {
107
+ const aa = mod(a, P);
108
+ if (aa === 0n) return 0n;
109
+ let x = modPow(aa, (P + 3n) / 8n, P);
110
+ if ((x * x) % P === aa) return x;
111
+ x = mod(x * SQRT_M1, P);
112
+ if ((x * x) % P === aa) return x;
113
+ return null;
114
+ }
115
+
116
+ // ── Byte/BigInt helpers (big-endian, matching OpenSSL BN_bin2bn) ─────────────
117
+
118
+ export function bytesToBigIntBE(bytes: Uint8Array): bigint {
119
+ let value = 0n;
120
+ for (const byte of bytes) value = (value << 8n) | BigInt(byte);
121
+ return value;
122
+ }
123
+
124
+ export function bigIntToBytesBE(value: bigint, length: number): Uint8Array {
125
+ const out = new Uint8Array(length);
126
+ let v = value;
127
+ for (let i = length - 1; i >= 0; i -= 1) {
128
+ out[i] = Number(v & 0xffn);
129
+ v >>= 8n;
130
+ }
131
+ return out;
132
+ }
133
+
134
+ export function sha256(...chunks: readonly Uint8Array[]): Uint8Array {
135
+ const hash = createHash("sha256");
136
+ for (const chunk of chunks) hash.update(chunk);
137
+ return new Uint8Array(hash.digest());
138
+ }
139
+
140
+ export function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
141
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
142
+ const out = new Uint8Array(total);
143
+ let cursor = 0;
144
+ for (const part of parts) {
145
+ out.set(part, cursor);
146
+ cursor += part.length;
147
+ }
148
+ return out;
149
+ }
150
+
151
+ // ── Point arithmetic (short Weierstrass: y² = x³ + A·x + B mod P) ────────────
152
+
153
+ /** Is the point on the curve? (Infinity counts as on-curve.) */
154
+ export function isOnCurve(point: Point): boolean {
155
+ if (point === null) return true;
156
+ const lhs = mod(point.y * point.y, P);
157
+ const rhs = mod(point.x * point.x * point.x + A * point.x + B, P);
158
+ return lhs === rhs;
159
+ }
160
+
161
+ export function pointAdd(p: Point, q: Point): Point {
162
+ if (p === null) return q;
163
+ if (q === null) return p;
164
+ if (p.x === q.x) {
165
+ if (mod(p.y + q.y, P) === 0n) return null; // p + (−p) = ∞
166
+ return pointDouble(p);
167
+ }
168
+ const slope = mod((q.y - p.y) * modInv(mod(q.x - p.x, P)), P);
169
+ const x = mod(slope * slope - p.x - q.x, P);
170
+ const y = mod(slope * (p.x - x) - p.y, P);
171
+ return { x, y };
172
+ }
173
+
174
+ export function pointDouble(p: Point): Point {
175
+ if (p === null) return null;
176
+ if (p.y === 0n) return null;
177
+ const slope = mod((3n * p.x * p.x + A) * modInv(mod(2n * p.y, P)), P);
178
+ const x = mod(slope * slope - 2n * p.x, P);
179
+ const y = mod(slope * (p.x - x) - p.y, P);
180
+ return { x, y };
181
+ }
182
+
183
+ export function scalarMul(scalar: bigint, point: Point): Point {
184
+ let result: Point = null;
185
+ let addend = point;
186
+ let k = scalar;
187
+ while (k > 0n) {
188
+ if (k & 1n) result = pointAdd(result, addend);
189
+ addend = pointDouble(addend);
190
+ k >>= 1n;
191
+ }
192
+ return result;
193
+ }
194
+
195
+ /**
196
+ * Recover the point with the given Weierstrass X and Y parity (LSB of Y), or
197
+ * null if X is not on the curve. Mirrors OpenSSL
198
+ * `EC_POINT_set_compressed_coordinates`.
199
+ */
200
+ export function liftX(xWeier: bigint, parity: number): Point {
201
+ const x = mod(xWeier, P);
202
+ const rhs = mod(x * x * x + A * x + B, P);
203
+ const y0 = modSqrt(rhs);
204
+ if (y0 === null) return null;
205
+ const y = Number(y0 & 1n) === (parity & 1) ? y0 : mod(-y0, P);
206
+ return { x, y };
207
+ }
208
+
209
+ // ── Point encoding: 32-byte big-endian Montgomery X + 1 parity byte ──────────
210
+
211
+ export function encodePoint(point: Point): Uint8Array {
212
+ if (point === null) {
213
+ throw new CentrsError({
214
+ code: "routeros/ec-srp5-protocol",
215
+ summary: "Cannot encode the EC-SRP5 point at infinity.",
216
+ remediation: "Regenerate the keypair.",
217
+ });
218
+ }
219
+ const out = new Uint8Array(EC_SRP5_PUBKEY_LEN);
220
+ out.set(bigIntToBytesBE(mod(point.x + W2M, P), 32), 0);
221
+ out[32] = Number(point.y & 1n);
222
+ return out;
223
+ }
224
+
225
+ export function decodePoint(key: Uint8Array): Point {
226
+ if (key.length !== EC_SRP5_PUBKEY_LEN) {
227
+ throw new CentrsError({
228
+ code: "routeros/ec-srp5-protocol",
229
+ summary: `EC-SRP5 public key must be ${EC_SRP5_PUBKEY_LEN} bytes (got ${key.length}).`,
230
+ remediation: "The peer sent a malformed key; confirm the device.",
231
+ });
232
+ }
233
+ const xWeier = mod(bytesToBigIntBE(key.subarray(0, 32)) + M2W, P);
234
+ const point = liftX(xWeier, key[32] as number);
235
+ if (point === null) {
236
+ throw new CentrsError({
237
+ code: "routeros/ec-srp5-protocol",
238
+ summary: "EC-SRP5 public key X is not a valid curve point.",
239
+ remediation: "The peer sent a malformed key; confirm the device.",
240
+ });
241
+ }
242
+ return point;
243
+ }
244
+
245
+ // ── EC-SRP5 primitives (ported from mtwei.c) ────────────────────────────────
246
+
247
+ export interface EcSrp5Keypair {
248
+ /** Private scalar (clamped). */
249
+ privateKey: bigint;
250
+ /** 33-byte public key (Montgomery X + parity). */
251
+ publicKey: Uint8Array;
252
+ }
253
+
254
+ /**
255
+ * Generate a keypair. The 32-byte private scalar is clamped exactly as
256
+ * `mtwei_keygen` does, then the public point is `priv·G` encoded to 33 bytes.
257
+ * Pass `privBytes` for deterministic tests; random otherwise.
258
+ */
259
+ export function ecSrp5Keygen(privBytes?: Uint8Array): EcSrp5Keypair {
260
+ const priv = privBytes
261
+ ? Uint8Array.from(privBytes)
262
+ : new Uint8Array(randomBytes(32));
263
+ if (priv.length !== 32) {
264
+ throw new CentrsError({
265
+ code: "routeros/ec-srp5-protocol",
266
+ summary: "EC-SRP5 private key seed must be 32 bytes.",
267
+ remediation: "Pass a 32-byte seed or omit it for a random key.",
268
+ });
269
+ }
270
+ // Clamp (verbatim byte ops from mtwei_keygen, interpreted big-endian).
271
+ priv[0] = (priv[0] as number) & 248;
272
+ priv[31] = (priv[31] as number) & 127;
273
+ priv[31] = (priv[31] as number) | 64;
274
+ const privateKey = bytesToBigIntBE(priv);
275
+ return { privateKey, publicKey: encodePoint(scalarMul(privateKey, G)) };
276
+ }
277
+
278
+ /**
279
+ * SRP identity validator: `SHA256(salt ‖ SHA256(username ‖ ":" ‖ password))`.
280
+ * (`mtwei_id`.) The caller passes the login name with any `+...` console-param
281
+ * suffix already stripped.
282
+ */
283
+ export function ecSrp5Id(
284
+ username: string,
285
+ password: string,
286
+ salt: Uint8Array,
287
+ ): Uint8Array {
288
+ const inner = sha256(new TextEncoder().encode(`${username}:${password}`));
289
+ return sha256(salt, inner);
290
+ }
291
+
292
+ /**
293
+ * REDP1 (IEEE P1363.2): deterministically derive a curve point from a 32-byte
294
+ * Montgomery-X seed, incrementing until X lands on the curve. Mirrors the loop
295
+ * inside `mtwei.c`'s `tangle`.
296
+ */
297
+ export function redp1(montgomeryX: Uint8Array, parity: number): Point {
298
+ let seed = bytesToBigIntBE(sha256(montgomeryX));
299
+ for (;;) {
300
+ const candidate = sha256(bigIntToBytesBE(seed, 32));
301
+ const xWeier = mod(bytesToBigIntBE(candidate) + M2W, P);
302
+ const point = liftX(xWeier, parity);
303
+ if (point !== null) return point;
304
+ seed = mod(seed + 1n, 1n << 256n);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Shared-secret material `(j, z)` common to both EC-SRP5 roles.
310
+ * j = SHA256(clientMontX ‖ serverMontX) (the two 32-byte Montgomery Xs)
311
+ * z = Montgomery-X of the agreed shared point
312
+ * The confirmation codes are derived from these (see {@link ecSrp5ClientConfirm}
313
+ * / {@link ecSrp5ServerConfirm}).
314
+ */
315
+ export interface EcSrp5Shared {
316
+ j: Uint8Array;
317
+ z: Uint8Array;
318
+ }
319
+
320
+ /**
321
+ * **Client** shared secret (ECPESVDP-SRP-A).
322
+ *
323
+ * v = REDP1(MontX(validator·G), parity 1) — undoes the server's blind
324
+ * w_b = serverPoint + v — = serverEphemeral·G
325
+ * j = SHA256(clientMontX ‖ serverMontX)
326
+ * scalar = (validator·j + priv) mod N
327
+ * z = MontX( scalar · w_b )
328
+ *
329
+ * `validator` is the `ecSrp5Id` identity (the scalar `i`), `serverKey` the peer's
330
+ * 33-byte (possibly blinded) public key, `clientKey` our own 33-byte public key.
331
+ */
332
+ export function ecSrp5ClientShared(
333
+ privateKey: bigint,
334
+ serverKey: Uint8Array,
335
+ clientKey: Uint8Array,
336
+ validator: Uint8Array,
337
+ ): EcSrp5Shared {
338
+ const serverPoint = decodePoint(serverKey);
339
+ const v = bytesToBigIntBE(validator);
340
+
341
+ // tangle: mix REDP1(v·G) into the server point (parity 1 for the client).
342
+ const vG = scalarMul(v, G);
343
+ if (vG === null) {
344
+ throw new CentrsError({
345
+ code: "routeros/ec-srp5-protocol",
346
+ summary: "EC-SRP5 validator produced the point at infinity.",
347
+ remediation: "Retry; this is astronomically unlikely.",
348
+ });
349
+ }
350
+ const gamma = redp1(bigIntToBytesBE(mod(vG.x + W2M, P), 32), 1);
351
+ const wB = pointAdd(serverPoint, gamma);
352
+
353
+ const j = sha256(clientKey.subarray(0, 32), serverKey.subarray(0, 32));
354
+ const scalar = mod(v * bytesToBigIntBE(j) + privateKey, N);
355
+ const pt = scalarMul(scalar, wB);
356
+ if (pt === null) {
357
+ throw new CentrsError({
358
+ code: "routeros/ec-srp5-protocol",
359
+ summary: "EC-SRP5 shared point computed as infinity.",
360
+ remediation: "The peer key may be invalid; confirm the device.",
361
+ });
362
+ }
363
+ const z = bigIntToBytesBE(mod(pt.x + W2M, P), 32);
364
+ return { j, z };
365
+ }
366
+
367
+ /**
368
+ * Compute the 32-byte EC-SRP5 **client** authentication proof
369
+ * (`mtwei_docrypto`): `SHA256(j ‖ z)`. Unchanged in bytes from the original
370
+ * `mtwei.c` port — MAC-Telnet's MTWEI uses this directly.
371
+ */
372
+ export function ecSrp5ClientProof(
373
+ privateKey: bigint,
374
+ serverKey: Uint8Array,
375
+ clientKey: Uint8Array,
376
+ validator: Uint8Array,
377
+ ): Uint8Array {
378
+ const { j, z } = ecSrp5ClientShared(
379
+ privateKey,
380
+ serverKey,
381
+ clientKey,
382
+ validator,
383
+ );
384
+ return sha256(j, z);
385
+ }
386
+
387
+ /**
388
+ * **Server** blinded public key to advertise (MikroTik btest MSG2 /
389
+ * `mtwei_docryptos`): `W_b = serverPrivate·G + REDP1(MontX(validator·G), 0)`.
390
+ * The validator-derived blind hides whether the password is known; the client
391
+ * removes it with the opposite-parity REDP1 (which is the point negation, so the
392
+ * two cancel to `serverPrivate·G`). Returns the 33-byte encoded point.
393
+ */
394
+ export function ecSrp5ServerPublicKey(
395
+ serverPrivate: bigint,
396
+ validatorScalar: bigint,
397
+ ): Uint8Array {
398
+ const pubB = scalarMul(serverPrivate, G);
399
+ const validatorPoint = scalarMul(validatorScalar, G);
400
+ if (pubB === null || validatorPoint === null) {
401
+ throw new CentrsError({
402
+ code: "routeros/ec-srp5-protocol",
403
+ summary: "EC-SRP5 server key produced the point at infinity.",
404
+ remediation: "Regenerate the server keypair.",
405
+ });
406
+ }
407
+ const xGamma = encodePoint(validatorPoint).subarray(0, 32);
408
+ const wB = pointAdd(pubB, redp1(xGamma, 0));
409
+ return encodePoint(wB);
410
+ }
411
+
412
+ /**
413
+ * **Server** shared secret (ECPESVDP-SRP-B).
414
+ *
415
+ * gamma = validator·G — the *raw* validator point (NOT REDP1; that
416
+ * only blinds the advertised key)
417
+ * j = SHA256(clientMontX ‖ serverMontX)
418
+ * z = MontX( serverPrivate · (clientPoint + j·gamma) )
419
+ *
420
+ * `serverKey` is the blinded key from {@link ecSrp5ServerPublicKey} (so `j`
421
+ * matches the client's), `clientKey` the peer's 33-byte public key.
422
+ */
423
+ export function ecSrp5ServerShared(
424
+ serverPrivate: bigint,
425
+ validatorScalar: bigint,
426
+ clientKey: Uint8Array,
427
+ serverKey: Uint8Array,
428
+ ): EcSrp5Shared {
429
+ const wA = decodePoint(clientKey);
430
+ const gamma = scalarMul(validatorScalar, G);
431
+ if (gamma === null) {
432
+ throw new CentrsError({
433
+ code: "routeros/ec-srp5-protocol",
434
+ summary: "EC-SRP5 validator produced the point at infinity.",
435
+ remediation: "The credentials are invalid; reconfigure the server.",
436
+ });
437
+ }
438
+ const j = sha256(clientKey.subarray(0, 32), serverKey.subarray(0, 32));
439
+ const sum = pointAdd(wA, scalarMul(bytesToBigIntBE(j), gamma));
440
+ const pt = scalarMul(serverPrivate, sum);
441
+ if (pt === null) {
442
+ throw new CentrsError({
443
+ code: "routeros/ec-srp5-protocol",
444
+ summary: "EC-SRP5 shared point computed as infinity.",
445
+ remediation: "The peer key may be invalid; confirm the client.",
446
+ });
447
+ }
448
+ const z = bigIntToBytesBE(mod(pt.x + W2M, P), 32);
449
+ return { j, z };
450
+ }
451
+
452
+ /** Client confirmation code: `SHA256(j ‖ z)` (equals {@link ecSrp5ClientProof}). */
453
+ export function ecSrp5ClientConfirm(shared: EcSrp5Shared): Uint8Array {
454
+ return sha256(shared.j, shared.z);
455
+ }
456
+
457
+ /** Server confirmation code: `SHA256(j ‖ clientConfirm ‖ z)`. */
458
+ export function ecSrp5ServerConfirm(
459
+ shared: EcSrp5Shared,
460
+ clientConfirm: Uint8Array,
461
+ ): Uint8Array {
462
+ return sha256(shared.j, clientConfirm, shared.z);
463
+ }
464
+
465
+ /** Curve/order constants exposed for tests and protocol modules. */
466
+ export const ecSrp5Curve = { P, A, B, N, G } as const;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Protocol adapter registry.
3
+ *
4
+ * Typed seam that adapters plug into so CLI, MCP, TUI, and proxy frontends
5
+ * can discover adapters, capabilities, and `via` identifiers from one source.
6
+ *
7
+ * Status per protocol is the implementation reality (`implemented` flag).
8
+ * Per-cell status across commands is in `docs/MATRIX.md`. Selection rules
9
+ * are in `docs/CONSTITUTION.md` (protocol selection).
10
+ */
11
+
12
+ export type ProtocolCapability =
13
+ | "execute"
14
+ | "retrieve"
15
+ | "transfer"
16
+ | "terminal"
17
+ | "discover"
18
+ | "measure"
19
+ | "proxy";
20
+
21
+ export interface ProtocolPlan {
22
+ id: string;
23
+ capabilities: readonly ProtocolCapability[];
24
+ notes: string;
25
+ implemented: boolean;
26
+ }
27
+
28
+ export const protocolPlans = [
29
+ {
30
+ id: "rest-api",
31
+ capabilities: ["retrieve", "execute", "transfer", "proxy"],
32
+ notes:
33
+ "RouterOS /rest. retrieve coded; cell status tracked in docs/MATRIX.md. Other capabilities are staged.",
34
+ implemented: true,
35
+ },
36
+ {
37
+ id: "native-api",
38
+ capabilities: ["retrieve", "execute", "proxy"],
39
+ notes:
40
+ "Binary API (TCP 8728 / TLS 8729). Transport base implemented in native-api.ts (word/sentence codec, login, tagged talk); command wiring tracked in docs/MATRIX.md.",
41
+ implemented: true,
42
+ },
43
+ {
44
+ id: "ssh",
45
+ capabilities: ["execute", "terminal", "transfer"],
46
+ notes:
47
+ "Reuses host ssh client. Important execute/terminal path after grounding.",
48
+ implemented: false,
49
+ },
50
+ {
51
+ id: "snmp",
52
+ capabilities: ["retrieve"],
53
+ notes: "Read-only metrics and identification.",
54
+ implemented: false,
55
+ },
56
+ {
57
+ id: "mndp",
58
+ capabilities: ["discover"],
59
+ notes: "Passive hint source only; not authoritative inventory.",
60
+ implemented: false,
61
+ },
62
+ {
63
+ id: "mac-telnet",
64
+ capabilities: ["execute", "terminal"],
65
+ notes:
66
+ "Layer-2 execute path (UDP 20561). Codec + MD5/MTWEI session (mac-telnet.ts), interactive-console reader (mac-telnet-console.ts), and UDP transport + execute adapter wired into execute. terminal cell tracked in docs/MATRIX.md.",
67
+ implemented: true,
68
+ },
69
+ {
70
+ id: "romon",
71
+ capabilities: ["execute"],
72
+ notes:
73
+ "Routed management overlay for execute. Lower priority than mac-telnet.",
74
+ implemented: false,
75
+ },
76
+ {
77
+ id: "winbox-terminal",
78
+ capabilities: ["execute"],
79
+ notes:
80
+ "WinBox terminal protocol for execute. Lower priority than mac-telnet.",
81
+ implemented: false,
82
+ },
83
+ {
84
+ id: "btest",
85
+ capabilities: ["measure"],
86
+ notes:
87
+ "MikroTik bandwidth test (peer protocol, TCP/UDP 2000). Explicit-only — never in the execute/retrieve downgrade chains. v1: client + server, EC-SRP5 + unauthenticated, TCP+UDP. Server CHR-passed; cell status in docs/MATRIX.md (Peer measurement).",
88
+ implemented: true,
89
+ },
90
+ ] as const satisfies readonly ProtocolPlan[];
91
+
92
+ export type RouterOsProtocol = (typeof protocolPlans)[number]["id"];
93
+
94
+ export const plannedProtocols = protocolPlans.map(
95
+ (protocol) => protocol.id,
96
+ ) as readonly RouterOsProtocol[];
97
+
98
+ const protocolPlansById = Object.fromEntries(
99
+ protocolPlans.map((protocol) => [protocol.id, protocol]),
100
+ ) as Record<RouterOsProtocol, (typeof protocolPlans)[number]>;
101
+
102
+ export function getProtocolPlan(id: RouterOsProtocol): ProtocolPlan {
103
+ return protocolPlansById[id];
104
+ }
105
+
106
+ export function protocolsWithCapability(
107
+ capability: ProtocolCapability,
108
+ ): readonly ProtocolPlan[] {
109
+ return protocolPlans.filter((protocol) => {
110
+ const capabilities: readonly ProtocolCapability[] = protocol.capabilities;
111
+ return capabilities.includes(capability);
112
+ });
113
+ }
114
+
115
+ export * from "./adapter.ts";
116
+ export * from "./mac-telnet.ts";
117
+ export * from "./mtwei.ts";
118
+ export * from "./native-api.ts";