ssh2web 1.0.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.
- package/README.md +122 -0
- package/dist/auth/authstate.d.ts +14 -0
- package/dist/auth/authstate.d.ts.map +1 -0
- package/dist/auth/authstate.js +25 -0
- package/dist/auth/certparser.d.ts +9 -0
- package/dist/auth/certparser.d.ts.map +1 -0
- package/dist/auth/certparser.js +13 -0
- package/dist/auth/keys.d.ts +7 -0
- package/dist/auth/keys.d.ts.map +1 -0
- package/dist/auth/keys.js +18 -0
- package/dist/channel/channelstate.d.ts +18 -0
- package/dist/channel/channelstate.d.ts.map +1 -0
- package/dist/channel/channelstate.js +30 -0
- package/dist/connection/connectSSH.d.ts +8 -0
- package/dist/connection/connectSSH.d.ts.map +1 -0
- package/dist/connection/connectSSH.js +516 -0
- package/dist/connection/connectionstate.d.ts +23 -0
- package/dist/connection/connectionstate.d.ts.map +1 -0
- package/dist/connection/connectionstate.js +46 -0
- package/dist/connection/types.d.ts +17 -0
- package/dist/connection/types.d.ts.map +1 -0
- package/dist/connection/types.js +4 -0
- package/dist/crypto/arithmetic.d.ts +7 -0
- package/dist/crypto/arithmetic.d.ts.map +1 -0
- package/dist/crypto/arithmetic.js +34 -0
- package/dist/crypto/cipher.d.ts +9 -0
- package/dist/crypto/cipher.d.ts.map +1 -0
- package/dist/crypto/cipher.js +43 -0
- package/dist/crypto/digest.d.ts +6 -0
- package/dist/crypto/digest.d.ts.map +1 -0
- package/dist/crypto/digest.js +10 -0
- package/dist/crypto/keyexchange.d.ts +11 -0
- package/dist/crypto/keyexchange.d.ts.map +1 -0
- package/dist/crypto/keyexchange.js +35 -0
- package/dist/crypto/keys.d.ts +11 -0
- package/dist/crypto/keys.d.ts.map +1 -0
- package/dist/crypto/keys.js +27 -0
- package/dist/crypto/mac.d.ts +7 -0
- package/dist/crypto/mac.d.ts.map +1 -0
- package/dist/crypto/mac.js +17 -0
- package/dist/debug.d.ts +9 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +19 -0
- package/dist/domain/constants.d.ts +47 -0
- package/dist/domain/constants.d.ts.map +1 -0
- package/dist/domain/constants.js +46 -0
- package/dist/domain/errors.d.ts +25 -0
- package/dist/domain/errors.d.ts.map +1 -0
- package/dist/domain/errors.js +45 -0
- package/dist/domain/models.d.ts +60 -0
- package/dist/domain/models.d.ts.map +1 -0
- package/dist/domain/models.js +10 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/kex/builder.d.ts +2 -0
- package/dist/kex/builder.d.ts.map +1 -0
- package/dist/kex/builder.js +22 -0
- package/dist/kex/kexstate.d.ts +20 -0
- package/dist/kex/kexstate.d.ts.map +1 -0
- package/dist/kex/kexstate.js +35 -0
- package/dist/protocol/codec.d.ts +7 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/codec.js +35 -0
- package/dist/protocol/deserialization.d.ts +19 -0
- package/dist/protocol/deserialization.d.ts.map +1 -0
- package/dist/protocol/deserialization.js +53 -0
- package/dist/protocol/messages.d.ts +5 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +33 -0
- package/dist/protocol/serialization.d.ts +11 -0
- package/dist/protocol/serialization.d.ts.map +1 -0
- package/dist/protocol/serialization.js +69 -0
- package/dist/transport/transportcipher.d.ts +25 -0
- package/dist/transport/transportcipher.d.ts.map +1 -0
- package/dist/transport/transportcipher.js +129 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +8 -0
- package/package.json +46 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main SSH connection orchestrator.
|
|
3
|
+
* Entry point that coordinates all layers: domain, crypto, protocol, transport, state machines.
|
|
4
|
+
*/
|
|
5
|
+
import { createConnectionState } from "./connectionstate";
|
|
6
|
+
import { log } from "../debug";
|
|
7
|
+
import { parsePacket } from "../protocol/codec";
|
|
8
|
+
import { getMessageName } from "../protocol/messages";
|
|
9
|
+
import { readString, readBytes, readMpint } from "../protocol/deserialization";
|
|
10
|
+
import { writeString, writeBytes, writeUint32, writeBigintMpint, concat } from "../protocol/serialization";
|
|
11
|
+
import { computeSHA256 } from "../crypto/digest";
|
|
12
|
+
import { generateDHPrivate, computeDHSharedSecret, generateX25519KeyPair, computeX25519SharedSecret } from "../crypto/keyexchange";
|
|
13
|
+
import { deriveKeys } from "../crypto/keys";
|
|
14
|
+
import { createTransportCipher } from "../transport/transportcipher";
|
|
15
|
+
import { buildKexInit } from "../kex/builder";
|
|
16
|
+
import { parseCertBase64 } from "../auth/certparser";
|
|
17
|
+
import { buildPacket } from "../protocol/codec";
|
|
18
|
+
import { writeByteMpint } from "../protocol/serialization";
|
|
19
|
+
import { MacVerificationError } from "../domain/errors";
|
|
20
|
+
import { parsePemPrivateKey, ed25519Sign } from "../auth/keys";
|
|
21
|
+
import { SSH_MSG_KEXINIT, SSH_MSG_KEX_ECDH_INIT, SSH_MSG_KEXDH_INIT, SSH_MSG_KEX_ECDH_REPLY, SSH_MSG_KEXDH_REPLY, SSH_MSG_NEWKEYS, SSH_MSG_SERVICE_REQUEST, SSH_MSG_SERVICE_ACCEPT, SSH_MSG_USERAUTH_REQUEST, SSH_MSG_USERAUTH_FAILURE, SSH_MSG_USERAUTH_PK_OK, SSH_MSG_USERAUTH_SUCCESS, SSH_MSG_CHANNEL_OPEN, SSH_MSG_CHANNEL_OPEN_CONFIRMATION, SSH_MSG_CHANNEL_REQUEST, SSH_MSG_CHANNEL_DATA, SSH_MSG_CHANNEL_EXTENDED_DATA, SSH_MSG_CHANNEL_SUCCESS, SSH_MSG_CHANNEL_FAILURE, SSH_MSG_CHANNEL_WINDOW_ADJUST, SSH_MSG_GLOBAL_REQUEST, SSH_MSG_REQUEST_SUCCESS, SSH_MSG_REQUEST_FAILURE, SSH_MSG_EXT_INFO, CLIENT_IDENT, PREFERRED_KEX, PREFERRED_MAC, DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS, DEFAULT_TERMINAL_TYPE, DEFAULT_WINDOW_SIZE, KEX_TIMEOUT_MS, } from "../domain/constants";
|
|
22
|
+
export async function connectSSH(ws, creds, onError, options) {
|
|
23
|
+
const privKey = await parsePemPrivateKey(creds.privateKey);
|
|
24
|
+
const certParsed = parseCertBase64(creds.certificate);
|
|
25
|
+
const termCols = options?.cols ?? DEFAULT_TERMINAL_COLS;
|
|
26
|
+
const termRows = options?.rows ?? DEFAULT_TERMINAL_ROWS;
|
|
27
|
+
log("=== SSH Connection Starting ===");
|
|
28
|
+
const conn = new SSHConnectionOrchestrator(ws, creds, privKey, certParsed, termCols, termRows, onError, options);
|
|
29
|
+
await conn.start();
|
|
30
|
+
return conn.getPublicAPI();
|
|
31
|
+
}
|
|
32
|
+
class SSHConnectionOrchestrator {
|
|
33
|
+
constructor(ws, creds, privKey, certParsed, termCols, termRows, onError, options) {
|
|
34
|
+
this.dataCallback = null;
|
|
35
|
+
this.dataBuffer = [];
|
|
36
|
+
this.encryptedBuf = new Uint8Array(0);
|
|
37
|
+
this.identBuf = new Uint8Array(0);
|
|
38
|
+
this.sendPending = Promise.resolve();
|
|
39
|
+
this.draining = false;
|
|
40
|
+
this.useEncrypted = false;
|
|
41
|
+
this.kexTimeoutId = null;
|
|
42
|
+
this.fatalError = false;
|
|
43
|
+
this.channelId = 0;
|
|
44
|
+
this.peerChannelId = new Uint8Array(4);
|
|
45
|
+
this.shellSent = false;
|
|
46
|
+
this.flushCount = 0;
|
|
47
|
+
this.kexInitC = null;
|
|
48
|
+
this.kexInitS = null;
|
|
49
|
+
this.serverKexList = [];
|
|
50
|
+
this.negotiatedMac = null;
|
|
51
|
+
this.ephemeralPriv = null;
|
|
52
|
+
this.qc = null;
|
|
53
|
+
this.dhX = null;
|
|
54
|
+
this.dhE = null;
|
|
55
|
+
this.sessionId = null;
|
|
56
|
+
this.receivedPKOk = false;
|
|
57
|
+
this.ws = ws;
|
|
58
|
+
this.creds = creds;
|
|
59
|
+
this.privKey = privKey;
|
|
60
|
+
this.certParsed = certParsed;
|
|
61
|
+
this.termCols = termCols;
|
|
62
|
+
this.termRows = termRows;
|
|
63
|
+
this.onError = onError;
|
|
64
|
+
this.options = options;
|
|
65
|
+
this.connState = createConnectionState(DEFAULT_WINDOW_SIZE);
|
|
66
|
+
ws.binaryType = "arraybuffer";
|
|
67
|
+
ws.onmessage = (e) => this.handleMessage(e);
|
|
68
|
+
ws.onclose = (e) => this.handleClose(e);
|
|
69
|
+
ws.onerror = () => this.handleWSError();
|
|
70
|
+
}
|
|
71
|
+
async start() {
|
|
72
|
+
this.send(new TextEncoder().encode(CLIENT_IDENT + "\r\n"));
|
|
73
|
+
log("SEND client ident:", CLIENT_IDENT);
|
|
74
|
+
}
|
|
75
|
+
send(data) {
|
|
76
|
+
if (this.ws.readyState === WebSocket.OPEN)
|
|
77
|
+
this.ws.send(data);
|
|
78
|
+
}
|
|
79
|
+
setFatal(msg) {
|
|
80
|
+
if (!this.fatalError) {
|
|
81
|
+
this.fatalError = true;
|
|
82
|
+
this.onError?.(msg);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async sendEncrypted(payload) {
|
|
86
|
+
const prev = this.sendPending;
|
|
87
|
+
const doSend = async () => {
|
|
88
|
+
if (!this.connState.cipher)
|
|
89
|
+
return;
|
|
90
|
+
const { ciphertext } = await this.connState.cipher.encrypt(payload);
|
|
91
|
+
if (this.ws.readyState === WebSocket.OPEN)
|
|
92
|
+
this.ws.send(ciphertext);
|
|
93
|
+
};
|
|
94
|
+
const ourSend = prev.then(doSend);
|
|
95
|
+
this.sendPending = ourSend;
|
|
96
|
+
await ourSend;
|
|
97
|
+
}
|
|
98
|
+
flushBuf() {
|
|
99
|
+
if (this.dataBuffer.length > 0 && this.dataCallback) {
|
|
100
|
+
const s = new TextDecoder().decode(new Uint8Array(this.dataBuffer));
|
|
101
|
+
this.flushCount++;
|
|
102
|
+
log(">>> PROMPT/DATA TO TERMINAL <<<", this.flushCount, "chars=", this.dataBuffer.length);
|
|
103
|
+
this.dataCallback(s);
|
|
104
|
+
this.dataBuffer.length = 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
handleMessage(e) {
|
|
108
|
+
if (e.data instanceof ArrayBuffer) {
|
|
109
|
+
log("ws.onmessage bytes=", e.data.byteLength);
|
|
110
|
+
this.handleRawData(new Uint8Array(e.data)).catch((err) => {
|
|
111
|
+
log("handleRawData error:", err);
|
|
112
|
+
this.setFatal(err instanceof Error ? err.message : String(err));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async handleRawData(d) {
|
|
117
|
+
if (this.fatalError)
|
|
118
|
+
return;
|
|
119
|
+
if (!this.connState.serverIdent) {
|
|
120
|
+
const result = this.extractServerIdent(d);
|
|
121
|
+
if (!result)
|
|
122
|
+
return;
|
|
123
|
+
d = result;
|
|
124
|
+
}
|
|
125
|
+
if (!this.useEncrypted) {
|
|
126
|
+
await this.processUnencryptedPackets(d);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.encryptedBuf = concat(this.encryptedBuf, d);
|
|
130
|
+
await this.processEncryptedPackets();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
extractServerIdent(d) {
|
|
134
|
+
const combined = concat(this.identBuf, d);
|
|
135
|
+
const sshStart = combined.findIndex((_, i) => i <= combined.length - 4 &&
|
|
136
|
+
combined[i] === 0x53 &&
|
|
137
|
+
combined[i + 1] === 0x53 &&
|
|
138
|
+
combined[i + 2] === 0x48 &&
|
|
139
|
+
combined[i + 3] === 0x2d);
|
|
140
|
+
if (sshStart < 0) {
|
|
141
|
+
this.identBuf = combined;
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const afterIdent = combined.subarray(sshStart);
|
|
145
|
+
let end = afterIdent.findIndex((_, i) => i < afterIdent.length - 1 && afterIdent[i] === 0x0d && afterIdent[i + 1] === 0x0a);
|
|
146
|
+
let termLen = 2;
|
|
147
|
+
if (end < 0) {
|
|
148
|
+
end = afterIdent.findIndex((b) => b === 0x0a);
|
|
149
|
+
termLen = 1;
|
|
150
|
+
}
|
|
151
|
+
if (end < 0) {
|
|
152
|
+
this.identBuf = combined;
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
this.connState.serverIdent = new TextDecoder().decode(afterIdent.subarray(0, end));
|
|
156
|
+
log("RECV server ident:", this.connState.serverIdent);
|
|
157
|
+
this.identBuf = new Uint8Array(0);
|
|
158
|
+
const restStart = sshStart + end + termLen;
|
|
159
|
+
if (restStart < combined.length)
|
|
160
|
+
return combined.subarray(restStart);
|
|
161
|
+
return new Uint8Array(0);
|
|
162
|
+
}
|
|
163
|
+
async processUnencryptedPackets(d) {
|
|
164
|
+
let offset = 0;
|
|
165
|
+
while (offset < d.length) {
|
|
166
|
+
const result = parsePacket(d.subarray(offset));
|
|
167
|
+
if (!result)
|
|
168
|
+
break;
|
|
169
|
+
offset += result.consumed;
|
|
170
|
+
await this.processPayload(result.payload);
|
|
171
|
+
if (this.useEncrypted) {
|
|
172
|
+
this.encryptedBuf = concat(this.encryptedBuf, d.subarray(offset));
|
|
173
|
+
await this.processEncryptedPackets();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async processEncryptedPackets() {
|
|
179
|
+
if (this.draining)
|
|
180
|
+
return;
|
|
181
|
+
this.draining = true;
|
|
182
|
+
try {
|
|
183
|
+
while (true) {
|
|
184
|
+
if (!this.connState.cipher)
|
|
185
|
+
break;
|
|
186
|
+
let r;
|
|
187
|
+
try {
|
|
188
|
+
r = await this.connState.cipher.decrypt(this.encryptedBuf);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err instanceof MacVerificationError) {
|
|
192
|
+
this.setFatal("SSH MAC verification failed");
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
if (!r)
|
|
197
|
+
break;
|
|
198
|
+
this.encryptedBuf = this.encryptedBuf.subarray(r.consumed);
|
|
199
|
+
await this.processPayload(r.payload);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
this.draining = false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async processPayload(p) {
|
|
207
|
+
const msgType = p[0];
|
|
208
|
+
if (msgType === 2)
|
|
209
|
+
return;
|
|
210
|
+
if (msgType === 1) {
|
|
211
|
+
const reasonCode = p.length >= 5 ? new DataView(p.buffer, p.byteOffset + 1, 4).getUint32(0, false) : 0;
|
|
212
|
+
const desc = readString(p, 5);
|
|
213
|
+
const msg = desc ? desc.value : `reason_code=${reasonCode}`;
|
|
214
|
+
log("DISCONNECT from server:", msg, "reason_code=", reasonCode);
|
|
215
|
+
this.setFatal(`Server disconnected: ${msg}`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (msgType === 3) {
|
|
219
|
+
const rejectedSeq = p.length >= 5 ? new DataView(p.buffer, p.byteOffset + 1, 4).getUint32(0, false) : 0;
|
|
220
|
+
log("UNIMPLEMENTED: server rejected our packet seq=", rejectedSeq);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (msgType === 4)
|
|
224
|
+
return;
|
|
225
|
+
if (msgType === SSH_MSG_EXT_INFO) {
|
|
226
|
+
log("EXT_INFO received (ignored)");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (msgType === SSH_MSG_GLOBAL_REQUEST) {
|
|
230
|
+
const reqName = readString(p, 1);
|
|
231
|
+
if (!reqName)
|
|
232
|
+
return;
|
|
233
|
+
const wantReply = 1 + reqName.consumed < p.length && p[1 + reqName.consumed] !== 0;
|
|
234
|
+
if (wantReply) {
|
|
235
|
+
const ok = reqName.value === "keepalive@openssh.com";
|
|
236
|
+
const reply = new Uint8Array([ok ? SSH_MSG_REQUEST_SUCCESS : SSH_MSG_REQUEST_FAILURE]);
|
|
237
|
+
await this.sendEncrypted(reply);
|
|
238
|
+
log("GLOBAL_REQUEST", reqName.value, "->", ok ? "REQUEST_SUCCESS" : "REQUEST_FAILURE");
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
log("processPayload", getMessageName(msgType), "len=", p.length);
|
|
243
|
+
if (msgType === SSH_MSG_KEXINIT && !this.kexInitS) {
|
|
244
|
+
this.kexInitS = p;
|
|
245
|
+
let o = 1 + 16;
|
|
246
|
+
const serverKex = readString(p, o);
|
|
247
|
+
if (!serverKex) {
|
|
248
|
+
this.setFatal("kex init parse");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
o += serverKex.consumed;
|
|
252
|
+
this.serverKexList = serverKex.value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
253
|
+
const serverHostKey = readString(p, o);
|
|
254
|
+
if (!serverHostKey) {
|
|
255
|
+
this.setFatal("kex init server_host_key");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
o += serverHostKey.consumed;
|
|
259
|
+
const cipherCtos = readString(p, o);
|
|
260
|
+
if (!cipherCtos) {
|
|
261
|
+
this.setFatal("kex init cipher_ctos");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
o += cipherCtos.consumed;
|
|
265
|
+
const cipherStoc = readString(p, o);
|
|
266
|
+
if (!cipherStoc) {
|
|
267
|
+
this.setFatal("kex init cipher_stoc");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
o += cipherStoc.consumed;
|
|
271
|
+
const macCtos = readString(p, o);
|
|
272
|
+
if (!macCtos) {
|
|
273
|
+
this.setFatal("kex init mac_ctos");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
o += macCtos.consumed;
|
|
277
|
+
const macStoc = readString(p, o);
|
|
278
|
+
if (!macStoc) {
|
|
279
|
+
this.setFatal("kex init mac_stoc");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
o += macStoc.consumed;
|
|
283
|
+
const serverMacList = macStoc.value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
284
|
+
const ourMacList = PREFERRED_MAC.split(",").map((s) => s.trim()).filter(Boolean);
|
|
285
|
+
this.negotiatedMac = ourMacList.find((a) => serverMacList.includes(a)) ?? null;
|
|
286
|
+
if (!this.negotiatedMac) {
|
|
287
|
+
this.setFatal(`MAC negotiation failed: server supports ${serverMacList.slice(0, 3).join(",")}... we need hmac-sha2-256 or hmac-sha2-256-etm@openssh.com`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
log("MAC negotiated:", this.negotiatedMac);
|
|
291
|
+
const ourKexList = PREFERRED_KEX.split(",").map((s) => s.trim()).filter(Boolean);
|
|
292
|
+
const negotiated = ourKexList.find((a) => this.serverKexList.includes(a));
|
|
293
|
+
const isDH = negotiated === "diffie-hellman-group14-sha256";
|
|
294
|
+
const isCurve = negotiated === "curve25519-sha256" || negotiated === "curve25519-sha256@libssh.org";
|
|
295
|
+
if (!negotiated || (!isDH && !isCurve)) {
|
|
296
|
+
this.setFatal(`KEX negotiation failed: server supports ${this.serverKexList.slice(0, 3).join(",")}... we need curve25519 or diffie-hellman-group14-sha256`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
log("KEX negotiated:", negotiated);
|
|
300
|
+
const kexInit = buildKexInit();
|
|
301
|
+
this.kexInitC = kexInit;
|
|
302
|
+
this.send(buildPacket(kexInit));
|
|
303
|
+
log("SEND KEXINIT");
|
|
304
|
+
if (isDH) {
|
|
305
|
+
const { x, e } = await generateDHPrivate();
|
|
306
|
+
this.dhX = x;
|
|
307
|
+
this.dhE = e;
|
|
308
|
+
const initPayload = concat(new Uint8Array([SSH_MSG_KEXDH_INIT]), writeBigintMpint(e));
|
|
309
|
+
this.send(buildPacket(initPayload));
|
|
310
|
+
log("SEND KEXDH_INIT (diffie-hellman-group14-sha256)");
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
const kp = await generateX25519KeyPair();
|
|
314
|
+
this.ephemeralPriv = kp.privateKey;
|
|
315
|
+
this.qc = kp.publicKey;
|
|
316
|
+
const initPayload = concat(new Uint8Array([SSH_MSG_KEX_ECDH_INIT]), writeBytes(this.qc));
|
|
317
|
+
this.send(buildPacket(initPayload));
|
|
318
|
+
log("SEND KEX_ECDH_INIT");
|
|
319
|
+
}
|
|
320
|
+
this.kexTimeoutId = setTimeout(() => {
|
|
321
|
+
if (!this.sessionId)
|
|
322
|
+
console.warn("[SSH] No KEX reply after 8s - server may reject KEX or connection hung. Check backend proxy logs.");
|
|
323
|
+
}, KEX_TIMEOUT_MS);
|
|
324
|
+
}
|
|
325
|
+
else if (msgType === SSH_MSG_KEXDH_REPLY && this.dhX !== null && this.dhE !== null && this.kexInitC && this.kexInitS) {
|
|
326
|
+
if (this.kexTimeoutId)
|
|
327
|
+
clearTimeout(this.kexTimeoutId);
|
|
328
|
+
this.kexTimeoutId = null;
|
|
329
|
+
let o = 1;
|
|
330
|
+
const ks = readBytes(p, o);
|
|
331
|
+
if (!ks) {
|
|
332
|
+
this.setFatal("kex dh reply parse");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
o += ks.consumed;
|
|
336
|
+
const serverHostKey = ks.value;
|
|
337
|
+
const fMpint = readMpint(p, o);
|
|
338
|
+
if (!fMpint) {
|
|
339
|
+
this.setFatal("kex dh reply f");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
o += fMpint.consumed;
|
|
343
|
+
const f = fMpint.value;
|
|
344
|
+
const k = computeDHSharedSecret(f, this.dhX);
|
|
345
|
+
const vc = CLIENT_IDENT;
|
|
346
|
+
const vs = this.connState.serverIdent.replace(/\r?\n$/, "");
|
|
347
|
+
const h = await computeSHA256(concat(writeString(vc), writeString(vs), writeBytes(this.kexInitC), writeBytes(this.kexInitS), writeBytes(serverHostKey), writeBigintMpint(this.dhE), writeBigintMpint(f), writeBigintMpint(k)));
|
|
348
|
+
this.sessionId = h;
|
|
349
|
+
this.connState.sessionId = h;
|
|
350
|
+
const kEncoded = writeBigintMpint(k);
|
|
351
|
+
const { ivC, keyC, macC, ivS, keyS, macS } = await deriveKeys(kEncoded, h, this.sessionId);
|
|
352
|
+
const macEtm = this.negotiatedMac === "hmac-sha2-256-etm@openssh.com";
|
|
353
|
+
const cipher = await createTransportCipher(ivC, keyC, macC, ivS, keyS, macS, 3, 3, macEtm);
|
|
354
|
+
this.connState.cipher = cipher;
|
|
355
|
+
this.send(buildPacket(concat(new Uint8Array([SSH_MSG_NEWKEYS]))));
|
|
356
|
+
log("SEND NEWKEYS encryption on (DH)");
|
|
357
|
+
}
|
|
358
|
+
else if (msgType === SSH_MSG_KEX_ECDH_REPLY && this.ephemeralPriv && this.qc && this.kexInitC && this.kexInitS) {
|
|
359
|
+
if (this.kexTimeoutId)
|
|
360
|
+
clearTimeout(this.kexTimeoutId);
|
|
361
|
+
this.kexTimeoutId = null;
|
|
362
|
+
let o = 1;
|
|
363
|
+
const ks = readBytes(p, o);
|
|
364
|
+
if (!ks) {
|
|
365
|
+
this.setFatal("kex reply parse");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
o += ks.consumed;
|
|
369
|
+
const serverHostKey = ks.value;
|
|
370
|
+
const qsBytes = readBytes(p, o);
|
|
371
|
+
if (!qsBytes) {
|
|
372
|
+
this.setFatal("kex reply qs");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
o += qsBytes.consumed;
|
|
376
|
+
const qs = qsBytes.value;
|
|
377
|
+
const k = await computeX25519SharedSecret(this.ephemeralPriv, qs);
|
|
378
|
+
const vc = CLIENT_IDENT;
|
|
379
|
+
const vs = this.connState.serverIdent.replace(/\r?\n$/, "");
|
|
380
|
+
const h = await computeSHA256(concat(writeString(vc), writeString(vs), writeBytes(this.kexInitC), writeBytes(this.kexInitS), writeBytes(serverHostKey), writeBytes(this.qc), writeBytes(qs), writeByteMpint(k)));
|
|
381
|
+
this.sessionId = h;
|
|
382
|
+
this.connState.sessionId = h;
|
|
383
|
+
const kEncoded = writeByteMpint(k);
|
|
384
|
+
const { ivC, keyC, macC, ivS, keyS, macS } = await deriveKeys(kEncoded, h, this.sessionId);
|
|
385
|
+
const macEtm = this.negotiatedMac === "hmac-sha2-256-etm@openssh.com";
|
|
386
|
+
const cipher = await createTransportCipher(ivC, keyC, macC, ivS, keyS, macS, 3, 3, macEtm);
|
|
387
|
+
this.connState.cipher = cipher;
|
|
388
|
+
this.send(buildPacket(concat(new Uint8Array([SSH_MSG_NEWKEYS]))));
|
|
389
|
+
log("SEND NEWKEYS encryption on (curve)");
|
|
390
|
+
}
|
|
391
|
+
else if (msgType === SSH_MSG_NEWKEYS) {
|
|
392
|
+
this.useEncrypted = true;
|
|
393
|
+
const serviceReq = concat(new Uint8Array([SSH_MSG_SERVICE_REQUEST]), writeString("ssh-userauth"));
|
|
394
|
+
await this.sendEncrypted(serviceReq);
|
|
395
|
+
log("SEND ssh-userauth");
|
|
396
|
+
}
|
|
397
|
+
else if (msgType === SSH_MSG_SERVICE_ACCEPT) {
|
|
398
|
+
const signData = concat(writeBytes(this.sessionId), new Uint8Array([SSH_MSG_USERAUTH_REQUEST]), writeString(this.creds.username), writeString("ssh-connection"), writeString("publickey"), new Uint8Array([1]), writeString(this.certParsed.keyType), writeBytes(this.certParsed.certBlob));
|
|
399
|
+
const sig = await ed25519Sign(this.privKey, signData);
|
|
400
|
+
const sigAlg = this.certParsed.keyType.startsWith("ssh-ed25519") ? "ssh-ed25519" : this.certParsed.keyType;
|
|
401
|
+
const sigBlob = concat(writeString(sigAlg), writeBytes(sig));
|
|
402
|
+
const authReq = concat(new Uint8Array([SSH_MSG_USERAUTH_REQUEST]), writeString(this.creds.username), writeString("ssh-connection"), writeString("publickey"), new Uint8Array([1]), writeString(this.certParsed.keyType), writeBytes(this.certParsed.certBlob), writeBytes(sigBlob));
|
|
403
|
+
await this.sendEncrypted(authReq);
|
|
404
|
+
log("SEND USERAUTH_REQUEST (with sig, no PK_OK round)");
|
|
405
|
+
}
|
|
406
|
+
else if (msgType === SSH_MSG_USERAUTH_FAILURE) {
|
|
407
|
+
const methods = readString(p, 1);
|
|
408
|
+
const hint = this.receivedPKOk ? "signature verification failed" : "server rejected key/cert";
|
|
409
|
+
const msg = methods ? `Auth failed (${hint}): ${methods.value || "publickey"}` : `Auth failed (${hint})`;
|
|
410
|
+
log("USERAUTH_FAILURE methods=", methods?.value, "afterPKOk=", this.receivedPKOk);
|
|
411
|
+
this.setFatal(msg);
|
|
412
|
+
}
|
|
413
|
+
else if (msgType === SSH_MSG_USERAUTH_PK_OK) {
|
|
414
|
+
this.receivedPKOk = true;
|
|
415
|
+
const signData = concat(writeBytes(this.sessionId), new Uint8Array([SSH_MSG_USERAUTH_REQUEST]), writeString(this.creds.username), writeString("ssh-connection"), writeString("publickey"), new Uint8Array([1]), writeString(this.certParsed.keyType), writeBytes(this.certParsed.certBlob));
|
|
416
|
+
const sig = await ed25519Sign(this.privKey, signData);
|
|
417
|
+
const sigAlg = this.certParsed.keyType.startsWith("ssh-ed25519") ? "ssh-ed25519" : this.certParsed.keyType;
|
|
418
|
+
const sigBlob = concat(writeString(sigAlg), writeBytes(sig));
|
|
419
|
+
const authReq2 = concat(new Uint8Array([SSH_MSG_USERAUTH_REQUEST]), writeString(this.creds.username), writeString("ssh-connection"), writeString("publickey"), new Uint8Array([1]), writeString(this.certParsed.keyType), writeBytes(this.certParsed.certBlob), writeBytes(sigBlob));
|
|
420
|
+
await this.sendEncrypted(authReq2);
|
|
421
|
+
log("SEND USERAUTH_REQUEST (with sig)");
|
|
422
|
+
}
|
|
423
|
+
else if (msgType === SSH_MSG_USERAUTH_SUCCESS) {
|
|
424
|
+
const chOpen = concat(new Uint8Array([SSH_MSG_CHANNEL_OPEN]), writeString("session"), new Uint8Array([0, 0, 0, 1]), new Uint8Array([0, 0, 0x80, 0]), new Uint8Array([0, 0, 0x20, 0]));
|
|
425
|
+
await this.sendEncrypted(chOpen);
|
|
426
|
+
log("SEND CHANNEL_OPEN session");
|
|
427
|
+
}
|
|
428
|
+
else if (msgType === SSH_MSG_CHANNEL_OPEN_CONFIRMATION) {
|
|
429
|
+
this.channelId = new DataView(p.buffer, p.byteOffset + 1, 4).getUint32(0, false);
|
|
430
|
+
this.peerChannelId = p.slice(5, 9);
|
|
431
|
+
const cols = this.options?.cols ?? DEFAULT_TERMINAL_COLS;
|
|
432
|
+
const rows = this.options?.rows ?? DEFAULT_TERMINAL_ROWS;
|
|
433
|
+
const ptyReq = concat(new Uint8Array([SSH_MSG_CHANNEL_REQUEST]), this.peerChannelId, writeString("pty-req"), new Uint8Array([1]), writeString(DEFAULT_TERMINAL_TYPE), writeUint32(cols), writeUint32(rows), writeUint32(0), writeUint32(0), writeBytes(new Uint8Array([0])));
|
|
434
|
+
await this.sendEncrypted(ptyReq);
|
|
435
|
+
log("SEND pty-req (want_reply)");
|
|
436
|
+
}
|
|
437
|
+
else if ((msgType === SSH_MSG_CHANNEL_SUCCESS || msgType === SSH_MSG_CHANNEL_FAILURE) && !this.shellSent) {
|
|
438
|
+
if (msgType === SSH_MSG_CHANNEL_FAILURE) {
|
|
439
|
+
this.onError?.("PTY request denied — shell may have no prompt or echo. Check cert has permit-pty.");
|
|
440
|
+
this.options?.onPtyDenied?.();
|
|
441
|
+
}
|
|
442
|
+
const shellReq = concat(new Uint8Array([SSH_MSG_CHANNEL_REQUEST]), this.peerChannelId, writeString("shell"), new Uint8Array([1]));
|
|
443
|
+
await this.sendEncrypted(shellReq);
|
|
444
|
+
this.shellSent = true;
|
|
445
|
+
log("SEND shell >>> SESSION READY <<<");
|
|
446
|
+
}
|
|
447
|
+
else if (msgType === SSH_MSG_CHANNEL_DATA) {
|
|
448
|
+
const data = readBytes(p, 5);
|
|
449
|
+
if (data) {
|
|
450
|
+
log("CHANNEL_DATA received", data.value.length, "bytes");
|
|
451
|
+
for (let i = 0; i < data.value.length; i++)
|
|
452
|
+
this.dataBuffer.push(data.value[i]);
|
|
453
|
+
this.flushBuf();
|
|
454
|
+
if (data.value.length > 0) {
|
|
455
|
+
const windowAdj = concat(new Uint8Array([SSH_MSG_CHANNEL_WINDOW_ADJUST]), this.peerChannelId, writeUint32(data.value.length));
|
|
456
|
+
void this.sendEncrypted(windowAdj);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else if (msgType === SSH_MSG_CHANNEL_EXTENDED_DATA) {
|
|
461
|
+
const data = readBytes(p, 9);
|
|
462
|
+
if (data) {
|
|
463
|
+
log("CHANNEL_EXTENDED_DATA stderr bytes=", data.value.length);
|
|
464
|
+
for (let i = 0; i < data.value.length; i++)
|
|
465
|
+
this.dataBuffer.push(data.value[i]);
|
|
466
|
+
this.flushBuf();
|
|
467
|
+
if (data.value.length > 0) {
|
|
468
|
+
const windowAdj = concat(new Uint8Array([SSH_MSG_CHANNEL_WINDOW_ADJUST]), this.peerChannelId, writeUint32(data.value.length));
|
|
469
|
+
void this.sendEncrypted(windowAdj);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else if (msgType > 0 && msgType < 100) {
|
|
474
|
+
log("UNHANDLED msgType=", msgType, getMessageName(msgType), "len=", p.length);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
handleClose(e) {
|
|
478
|
+
log("ws.onclose code=", e.code, "reason=", e.reason);
|
|
479
|
+
if (!this.fatalError && e.code === 1000 && e.reason === "session ended") {
|
|
480
|
+
this.setFatal("Session ended by server");
|
|
481
|
+
}
|
|
482
|
+
else if (!this.fatalError && !e.wasClean) {
|
|
483
|
+
this.setFatal(`Connection closed: ${e.reason || `code ${e.code}`}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
handleWSError() {
|
|
487
|
+
log("ws.onerror");
|
|
488
|
+
}
|
|
489
|
+
getPublicAPI() {
|
|
490
|
+
return {
|
|
491
|
+
write: (data) => {
|
|
492
|
+
if (!this.shellSent)
|
|
493
|
+
return;
|
|
494
|
+
const b = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
495
|
+
const payload = new Uint8Array(1 + 4 + 4 + b.length);
|
|
496
|
+
payload[0] = SSH_MSG_CHANNEL_DATA;
|
|
497
|
+
payload.set(this.peerChannelId, 1);
|
|
498
|
+
new DataView(payload.buffer).setUint32(5, b.length, false);
|
|
499
|
+
payload.set(b, 9);
|
|
500
|
+
void this.sendEncrypted(payload);
|
|
501
|
+
},
|
|
502
|
+
onData: (cb) => {
|
|
503
|
+
log("onData callback registered");
|
|
504
|
+
this.dataCallback = cb;
|
|
505
|
+
this.flushBuf();
|
|
506
|
+
},
|
|
507
|
+
resize: (cols, rows) => {
|
|
508
|
+
if (this.channelId === 0)
|
|
509
|
+
return;
|
|
510
|
+
const winReq = concat(new Uint8Array([SSH_MSG_CHANNEL_REQUEST]), this.peerChannelId, writeString("window-change"), new Uint8Array([0]), writeUint32(cols), writeUint32(rows), writeUint32(0), writeUint32(0));
|
|
511
|
+
void this.sendEncrypted(winReq);
|
|
512
|
+
},
|
|
513
|
+
close: () => this.ws.close(),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overall connection state machine.
|
|
3
|
+
* Tracks: version exchange → kex → auth → channel open → data.
|
|
4
|
+
*/
|
|
5
|
+
import { KexState } from "../kex/kexstate";
|
|
6
|
+
import { AuthState } from "../auth/authstate";
|
|
7
|
+
import { ChannelState } from "../channel/channelstate";
|
|
8
|
+
import { TransportCipher } from "../transport/transportcipher";
|
|
9
|
+
export type ConnectionPhase = "ident_exchange" | "kex" | "auth" | "channel_open" | "active" | "closed" | "error";
|
|
10
|
+
export interface ConnectionState {
|
|
11
|
+
phase: ConnectionPhase;
|
|
12
|
+
serverIdent: string;
|
|
13
|
+
sessionId: Uint8Array | null;
|
|
14
|
+
kex: KexState;
|
|
15
|
+
auth: AuthState;
|
|
16
|
+
channel: ChannelState;
|
|
17
|
+
cipher: TransportCipher | null;
|
|
18
|
+
fatalError: string | null;
|
|
19
|
+
}
|
|
20
|
+
export declare function createConnectionState(defaultWindowSize: number): ConnectionState;
|
|
21
|
+
export declare function setFatalError(state: ConnectionState, error: string): ConnectionState;
|
|
22
|
+
export declare function transitionConnectionPhase(current: ConnectionPhase): ConnectionPhase;
|
|
23
|
+
//# sourceMappingURL=connectionstate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectionstate.d.ts","sourceRoot":"","sources":["../../connection/connectionstate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,KAAK,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,UAAU,GAAG,IAAI,CAAC;IAC7B,GAAG,EAAE,QAAQ,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,qBAAqB,CAAC,iBAAiB,EAAE,MAAM,GAAG,eAAe,CA8BhF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,CAEpF;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,eAAe,GAAG,eAAe,CAWnF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function createConnectionState(defaultWindowSize) {
|
|
2
|
+
return {
|
|
3
|
+
phase: "ident_exchange",
|
|
4
|
+
serverIdent: "",
|
|
5
|
+
sessionId: null,
|
|
6
|
+
kex: {
|
|
7
|
+
phase: "init",
|
|
8
|
+
serverKexList: [],
|
|
9
|
+
serverHostKeyTypes: [],
|
|
10
|
+
negotiated: null,
|
|
11
|
+
kexInitC: null,
|
|
12
|
+
kexInitS: null,
|
|
13
|
+
},
|
|
14
|
+
auth: {
|
|
15
|
+
phase: "init",
|
|
16
|
+
receivedPKOk: false,
|
|
17
|
+
error: null,
|
|
18
|
+
},
|
|
19
|
+
channel: {
|
|
20
|
+
phase: "init",
|
|
21
|
+
clientChannelId: 0,
|
|
22
|
+
serverChannelId: 0,
|
|
23
|
+
windowSizeLocal: defaultWindowSize,
|
|
24
|
+
windowSizeRemote: defaultWindowSize,
|
|
25
|
+
ptySent: false,
|
|
26
|
+
shellSent: false,
|
|
27
|
+
},
|
|
28
|
+
cipher: null,
|
|
29
|
+
fatalError: null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function setFatalError(state, error) {
|
|
33
|
+
return { ...state, phase: "error", fatalError: error };
|
|
34
|
+
}
|
|
35
|
+
export function transitionConnectionPhase(current) {
|
|
36
|
+
const transitions = {
|
|
37
|
+
"ident_exchange": "kex",
|
|
38
|
+
"kex": "auth",
|
|
39
|
+
"auth": "channel_open",
|
|
40
|
+
"channel_open": "active",
|
|
41
|
+
"active": "active",
|
|
42
|
+
"closed": "closed",
|
|
43
|
+
"error": "error",
|
|
44
|
+
};
|
|
45
|
+
return transitions[current];
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API types for SSH connection.
|
|
3
|
+
*/
|
|
4
|
+
import type { Credentials } from "../domain/models";
|
|
5
|
+
export type { Credentials as SSHCredentials };
|
|
6
|
+
export interface SSHConnection {
|
|
7
|
+
write: (data: string | Uint8Array) => void;
|
|
8
|
+
onData: (cb: (data: string) => void) => void;
|
|
9
|
+
resize: (cols: number, rows: number) => void;
|
|
10
|
+
close: () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface ConnectSSHOptions {
|
|
13
|
+
cols?: number;
|
|
14
|
+
rows?: number;
|
|
15
|
+
onPtyDenied?: () => void;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../connection/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,kBAAkB,CAAC;AAEpE,YAAY,EAAE,WAAW,IAAI,cAAc,EAAE,CAAC;AAE9C,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,KAAK,IAAI,CAAC;IAC3C,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC;IAC7C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modular arithmetic and bigint utilities for cryptographic operations.
|
|
3
|
+
*/
|
|
4
|
+
export declare function modPow(base: bigint, exp: bigint, mod: bigint): bigint;
|
|
5
|
+
export declare function bigintToBytes(n: bigint): Uint8Array;
|
|
6
|
+
export declare function bytesToBigint(b: Uint8Array): bigint;
|
|
7
|
+
//# sourceMappingURL=arithmetic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arithmetic.d.ts","sourceRoot":"","sources":["../../crypto/arithmetic.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CASrE;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CASnD;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAKnD"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modular arithmetic and bigint utilities for cryptographic operations.
|
|
3
|
+
*/
|
|
4
|
+
export function modPow(base, exp, mod) {
|
|
5
|
+
let result = 1n;
|
|
6
|
+
base = base % mod;
|
|
7
|
+
while (exp > 0n) {
|
|
8
|
+
if (exp % 2n === 1n)
|
|
9
|
+
result = (result * base) % mod;
|
|
10
|
+
exp = exp / 2n;
|
|
11
|
+
base = (base * base) % mod;
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
export function bigintToBytes(n) {
|
|
16
|
+
if (n === 0n)
|
|
17
|
+
return new Uint8Array([0]);
|
|
18
|
+
let hex = n.toString(16);
|
|
19
|
+
if (hex.length % 2)
|
|
20
|
+
hex = "0" + hex;
|
|
21
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
22
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
23
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
24
|
+
}
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
export function bytesToBigint(b) {
|
|
28
|
+
let n = 0n;
|
|
29
|
+
for (let i = 0; i < b.length; i++)
|
|
30
|
+
n = (n << 8n) | BigInt(b[i]);
|
|
31
|
+
if (b.length > 0 && (b[0] & 0x80))
|
|
32
|
+
n -= 1n << BigInt(b.length * 8);
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function encryptAES128CTR(key: CryptoKey, iv: Uint8Array, plaintext: Uint8Array): Promise<{
|
|
2
|
+
ciphertext: Uint8Array;
|
|
3
|
+
nextIv: Uint8Array;
|
|
4
|
+
}>;
|
|
5
|
+
export declare function decryptAES128CTR(key: CryptoKey, iv: Uint8Array, ciphertext: Uint8Array): Promise<{
|
|
6
|
+
plaintext: Uint8Array;
|
|
7
|
+
nextIv: Uint8Array;
|
|
8
|
+
}>;
|
|
9
|
+
//# sourceMappingURL=cipher.d.ts.map
|