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.
Files changed (80) hide show
  1. package/README.md +122 -0
  2. package/dist/auth/authstate.d.ts +14 -0
  3. package/dist/auth/authstate.d.ts.map +1 -0
  4. package/dist/auth/authstate.js +25 -0
  5. package/dist/auth/certparser.d.ts +9 -0
  6. package/dist/auth/certparser.d.ts.map +1 -0
  7. package/dist/auth/certparser.js +13 -0
  8. package/dist/auth/keys.d.ts +7 -0
  9. package/dist/auth/keys.d.ts.map +1 -0
  10. package/dist/auth/keys.js +18 -0
  11. package/dist/channel/channelstate.d.ts +18 -0
  12. package/dist/channel/channelstate.d.ts.map +1 -0
  13. package/dist/channel/channelstate.js +30 -0
  14. package/dist/connection/connectSSH.d.ts +8 -0
  15. package/dist/connection/connectSSH.d.ts.map +1 -0
  16. package/dist/connection/connectSSH.js +516 -0
  17. package/dist/connection/connectionstate.d.ts +23 -0
  18. package/dist/connection/connectionstate.d.ts.map +1 -0
  19. package/dist/connection/connectionstate.js +46 -0
  20. package/dist/connection/types.d.ts +17 -0
  21. package/dist/connection/types.d.ts.map +1 -0
  22. package/dist/connection/types.js +4 -0
  23. package/dist/crypto/arithmetic.d.ts +7 -0
  24. package/dist/crypto/arithmetic.d.ts.map +1 -0
  25. package/dist/crypto/arithmetic.js +34 -0
  26. package/dist/crypto/cipher.d.ts +9 -0
  27. package/dist/crypto/cipher.d.ts.map +1 -0
  28. package/dist/crypto/cipher.js +43 -0
  29. package/dist/crypto/digest.d.ts +6 -0
  30. package/dist/crypto/digest.d.ts.map +1 -0
  31. package/dist/crypto/digest.js +10 -0
  32. package/dist/crypto/keyexchange.d.ts +11 -0
  33. package/dist/crypto/keyexchange.d.ts.map +1 -0
  34. package/dist/crypto/keyexchange.js +35 -0
  35. package/dist/crypto/keys.d.ts +11 -0
  36. package/dist/crypto/keys.d.ts.map +1 -0
  37. package/dist/crypto/keys.js +27 -0
  38. package/dist/crypto/mac.d.ts +7 -0
  39. package/dist/crypto/mac.d.ts.map +1 -0
  40. package/dist/crypto/mac.js +17 -0
  41. package/dist/debug.d.ts +9 -0
  42. package/dist/debug.d.ts.map +1 -0
  43. package/dist/debug.js +19 -0
  44. package/dist/domain/constants.d.ts +47 -0
  45. package/dist/domain/constants.d.ts.map +1 -0
  46. package/dist/domain/constants.js +46 -0
  47. package/dist/domain/errors.d.ts +25 -0
  48. package/dist/domain/errors.d.ts.map +1 -0
  49. package/dist/domain/errors.js +45 -0
  50. package/dist/domain/models.d.ts +60 -0
  51. package/dist/domain/models.d.ts.map +1 -0
  52. package/dist/domain/models.js +10 -0
  53. package/dist/index.d.ts +8 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +6 -0
  56. package/dist/kex/builder.d.ts +2 -0
  57. package/dist/kex/builder.d.ts.map +1 -0
  58. package/dist/kex/builder.js +22 -0
  59. package/dist/kex/kexstate.d.ts +20 -0
  60. package/dist/kex/kexstate.d.ts.map +1 -0
  61. package/dist/kex/kexstate.js +35 -0
  62. package/dist/protocol/codec.d.ts +7 -0
  63. package/dist/protocol/codec.d.ts.map +1 -0
  64. package/dist/protocol/codec.js +35 -0
  65. package/dist/protocol/deserialization.d.ts +19 -0
  66. package/dist/protocol/deserialization.d.ts.map +1 -0
  67. package/dist/protocol/deserialization.js +53 -0
  68. package/dist/protocol/messages.d.ts +5 -0
  69. package/dist/protocol/messages.d.ts.map +1 -0
  70. package/dist/protocol/messages.js +33 -0
  71. package/dist/protocol/serialization.d.ts +11 -0
  72. package/dist/protocol/serialization.d.ts.map +1 -0
  73. package/dist/protocol/serialization.js +69 -0
  74. package/dist/transport/transportcipher.d.ts +25 -0
  75. package/dist/transport/transportcipher.d.ts.map +1 -0
  76. package/dist/transport/transportcipher.js +129 -0
  77. package/dist/vitest.config.d.ts +3 -0
  78. package/dist/vitest.config.d.ts.map +1 -0
  79. package/dist/vitest.config.js +8 -0
  80. package/package.json +46 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * RFC 4253 SSH binary packet format.
3
+ * Single responsibility: Packet structure encoding/decoding without encryption.
4
+ */
5
+ import { AES_BLOCK_SIZE, MIN_PADDING } from "../domain/constants";
6
+ export function buildPacket(payload, etm = false) {
7
+ const padLen = calculatePadding(payload.length, etm);
8
+ const plen = 1 + payload.length + padLen;
9
+ const out = new Uint8Array(4 + plen);
10
+ const view = new DataView(out.buffer);
11
+ view.setUint32(0, plen, false);
12
+ out[4] = padLen;
13
+ out.set(payload, 5);
14
+ crypto.getRandomValues(out.subarray(5 + payload.length, 5 + payload.length + padLen));
15
+ return out;
16
+ }
17
+ export function parsePacket(data) {
18
+ if (data.length < 5)
19
+ return null;
20
+ const plen = new DataView(data.buffer, data.byteOffset, 4).getUint32(0, false);
21
+ const total = 4 + plen;
22
+ if (data.length < total)
23
+ return null;
24
+ const padLen = data[4];
25
+ const payloadLen = plen - 1 - padLen;
26
+ const payload = data.slice(5, 5 + payloadLen);
27
+ return { payload, consumed: total };
28
+ }
29
+ function calculatePadding(payloadLen, etm) {
30
+ if (etm) {
31
+ const innerLen = 1 + payloadLen;
32
+ return MIN_PADDING + (AES_BLOCK_SIZE - ((innerLen + MIN_PADDING) % AES_BLOCK_SIZE)) % AES_BLOCK_SIZE;
33
+ }
34
+ return MIN_PADDING + (AES_BLOCK_SIZE - ((5 + payloadLen + MIN_PADDING) % AES_BLOCK_SIZE)) % AES_BLOCK_SIZE;
35
+ }
@@ -0,0 +1,19 @@
1
+ export interface StringValue {
2
+ value: string;
3
+ consumed: number;
4
+ }
5
+ export interface BytesValue {
6
+ value: Uint8Array;
7
+ consumed: number;
8
+ }
9
+ export interface BigintValue {
10
+ value: bigint;
11
+ consumed: number;
12
+ }
13
+ export declare function readString(data: Uint8Array, offset: number): StringValue | null;
14
+ export declare function readBytes(data: Uint8Array, offset: number): BytesValue | null;
15
+ export declare function readMpint(data: Uint8Array, offset: number): BigintValue | null;
16
+ export declare function readStringThrows(data: Uint8Array, offset: number): StringValue;
17
+ export declare function readBytesThrows(data: Uint8Array, offset: number): BytesValue;
18
+ export declare function readMpintThrows(data: Uint8Array, offset: number): BigintValue;
19
+ //# sourceMappingURL=deserialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deserialization.d.ts","sourceRoot":"","sources":["../../protocol/deserialization.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAM/E;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAM7E;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAQ9E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAI9E;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAI5E;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAI7E"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * SSH protocol deserialization (reading data types from packets).
3
+ * Single responsibility: Decoding strings, bytes, and integers.
4
+ */
5
+ import { ParseError } from "../domain/errors";
6
+ export function readString(data, offset) {
7
+ if (offset + 4 > data.length)
8
+ return null;
9
+ const len = new DataView(data.buffer, data.byteOffset + offset, 4).getUint32(0, false);
10
+ if (offset + 4 + len > data.length)
11
+ return null;
12
+ const value = new TextDecoder().decode(data.subarray(offset + 4, offset + 4 + len));
13
+ return { value, consumed: 4 + len };
14
+ }
15
+ export function readBytes(data, offset) {
16
+ if (offset + 4 > data.length)
17
+ return null;
18
+ const len = new DataView(data.buffer, data.byteOffset + offset, 4).getUint32(0, false);
19
+ if (offset + 4 + len > data.length)
20
+ return null;
21
+ const value = data.slice(offset + 4, offset + 4 + len);
22
+ return { value, consumed: 4 + len };
23
+ }
24
+ export function readMpint(data, offset) {
25
+ const rb = readBytes(data, offset);
26
+ if (!rb)
27
+ return null;
28
+ const b = rb.value;
29
+ let n = 0n;
30
+ for (let i = 0; i < b.length; i++)
31
+ n = (n << 8n) | BigInt(b[i]);
32
+ if (b.length > 0 && (b[0] & 0x80))
33
+ n -= 1n << BigInt(b.length * 8);
34
+ return { value: n, consumed: rb.consumed };
35
+ }
36
+ export function readStringThrows(data, offset) {
37
+ const result = readString(data, offset);
38
+ if (!result)
39
+ throw new ParseError(`Failed to read string at offset ${offset}`);
40
+ return result;
41
+ }
42
+ export function readBytesThrows(data, offset) {
43
+ const result = readBytes(data, offset);
44
+ if (!result)
45
+ throw new ParseError(`Failed to read bytes at offset ${offset}`);
46
+ return result;
47
+ }
48
+ export function readMpintThrows(data, offset) {
49
+ const result = readMpint(data, offset);
50
+ if (!result)
51
+ throw new ParseError(`Failed to read mpint at offset ${offset}`);
52
+ return result;
53
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * SSH message type utilities and debug information.
3
+ */
4
+ export declare function getMessageName(msgType: number): string;
5
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../protocol/messages.ts"],"names":[],"mappings":"AAAA;;GAEG;AA8BH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * SSH message type utilities and debug information.
3
+ */
4
+ const MESSAGE_NAMES = {
5
+ 1: "DISCONNECT",
6
+ 3: "UNIMPLEMENTED",
7
+ 4: "DEBUG",
8
+ 5: "SERVICE_REQUEST",
9
+ 6: "SERVICE_ACCEPT",
10
+ 7: "EXT_INFO",
11
+ 20: "KEXINIT",
12
+ 21: "NEWKEYS",
13
+ 30: "KEX_DH_INIT",
14
+ 31: "KEX_DH_REPLY",
15
+ 50: "USERAUTH_REQUEST",
16
+ 51: "USERAUTH_FAILURE",
17
+ 52: "USERAUTH_SUCCESS",
18
+ 60: "USERAUTH_PK_OK",
19
+ 80: "GLOBAL_REQUEST",
20
+ 81: "REQUEST_SUCCESS",
21
+ 82: "REQUEST_FAILURE",
22
+ 90: "CHANNEL_OPEN",
23
+ 91: "CHANNEL_OPEN_CONFIRMATION",
24
+ 93: "CHANNEL_WINDOW_ADJUST",
25
+ 94: "CHANNEL_DATA",
26
+ 95: "CHANNEL_EXTENDED_DATA",
27
+ 98: "CHANNEL_REQUEST",
28
+ 99: "CHANNEL_SUCCESS",
29
+ 100: "CHANNEL_FAILURE",
30
+ };
31
+ export function getMessageName(msgType) {
32
+ return MESSAGE_NAMES[msgType] ?? `msg${msgType}`;
33
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * SSH protocol serialization (writing data types to packets).
3
+ * Single responsibility: Encoding strings, bytes, and integers.
4
+ */
5
+ export declare function writeString(s: string): Uint8Array;
6
+ export declare function writeBytes(b: Uint8Array): Uint8Array;
7
+ export declare function writeUint32(n: number): Uint8Array;
8
+ export declare function writeBigintMpint(n: bigint): Uint8Array;
9
+ export declare function writeByteMpint(k: Uint8Array): Uint8Array;
10
+ export declare function concat(...arr: Uint8Array[]): Uint8Array;
11
+ //# sourceMappingURL=serialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialization.d.ts","sourceRoot":"","sources":["../../protocol/serialization.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAMjD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAKpD;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAIjD;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAYtD;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CASxD;AAED,wBAAgB,MAAM,CAAC,GAAG,GAAG,EAAE,UAAU,EAAE,GAAG,UAAU,CASvD"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * SSH protocol serialization (writing data types to packets).
3
+ * Single responsibility: Encoding strings, bytes, and integers.
4
+ */
5
+ export function writeString(s) {
6
+ const enc = new TextEncoder().encode(s);
7
+ const out = new Uint8Array(4 + enc.length);
8
+ new DataView(out.buffer).setUint32(0, enc.length, false);
9
+ out.set(enc, 4);
10
+ return out;
11
+ }
12
+ export function writeBytes(b) {
13
+ const out = new Uint8Array(4 + b.length);
14
+ new DataView(out.buffer).setUint32(0, b.length, false);
15
+ out.set(b, 4);
16
+ return out;
17
+ }
18
+ export function writeUint32(n) {
19
+ const out = new Uint8Array(4);
20
+ new DataView(out.buffer).setUint32(0, n, false);
21
+ return out;
22
+ }
23
+ export function writeBigintMpint(n) {
24
+ if (n === 0n)
25
+ return writeBytes(new Uint8Array(0));
26
+ const neg = n < 0n;
27
+ if (neg)
28
+ n = -n;
29
+ let bytes = bigintToBytes(n);
30
+ if (bytes[0] & 0x80) {
31
+ const tmp = new Uint8Array(bytes.length + 1);
32
+ tmp[0] = 0;
33
+ tmp.set(bytes, 1);
34
+ bytes = tmp;
35
+ }
36
+ return writeBytes(bytes);
37
+ }
38
+ export function writeByteMpint(k) {
39
+ let kk = k;
40
+ if (kk.length > 0 && (kk[0] & 0x80) !== 0) {
41
+ const tmp = new Uint8Array(kk.length + 1);
42
+ tmp[0] = 0;
43
+ tmp.set(kk, 1);
44
+ kk = tmp;
45
+ }
46
+ return writeBytes(kk);
47
+ }
48
+ export function concat(...arr) {
49
+ const len = arr.reduce((a, b) => a + b.length, 0);
50
+ const out = new Uint8Array(len);
51
+ let off = 0;
52
+ for (const a of arr) {
53
+ out.set(a, off);
54
+ off += a.length;
55
+ }
56
+ return out;
57
+ }
58
+ function bigintToBytes(n) {
59
+ if (n === 0n)
60
+ return new Uint8Array([0]);
61
+ let hex = n.toString(16);
62
+ if (hex.length % 2)
63
+ hex = "0" + hex;
64
+ const bytes = new Uint8Array(hex.length / 2);
65
+ for (let i = 0; i < bytes.length; i++) {
66
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
67
+ }
68
+ return bytes;
69
+ }
@@ -0,0 +1,25 @@
1
+ export interface EncryptResult {
2
+ ciphertext: Uint8Array;
3
+ }
4
+ export interface DecryptResult {
5
+ payload: Uint8Array;
6
+ consumed: number;
7
+ }
8
+ export declare class TransportCipher {
9
+ private ivEnc;
10
+ private ivDec;
11
+ private keyEnc;
12
+ private keyDec;
13
+ private seqOut;
14
+ private seqIn;
15
+ private macEtm;
16
+ private macC;
17
+ private macS;
18
+ constructor(ivC: Uint8Array, keyC: Uint8Array, macC: Uint8Array, ivS: Uint8Array, keyS: Uint8Array, macS: Uint8Array, initialSeqOut: number, initialSeqIn: number, macEtm: boolean, keyEncCrypto: CryptoKey, keyDecCrypto: CryptoKey);
19
+ encrypt(payload: Uint8Array): Promise<EncryptResult>;
20
+ decrypt(data: Uint8Array): Promise<DecryptResult | null>;
21
+ private decryptEtm;
22
+ private decryptStandard;
23
+ }
24
+ export declare function createTransportCipher(ivC: Uint8Array, keyC: Uint8Array, macC: Uint8Array, ivS: Uint8Array, keyS: Uint8Array, macS: Uint8Array, initialSeqOut: number, initialSeqIn: number, macEtm: boolean): Promise<TransportCipher>;
25
+ //# sourceMappingURL=transportcipher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transportcipher.d.ts","sourceRoot":"","sources":["../../transport/transportcipher.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,UAAU,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,IAAI,CAAa;gBAGvB,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,UAAU,EAChB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,OAAO,EACf,YAAY,EAAE,SAAS,EACvB,YAAY,EAAE,SAAS;IAanB,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC;IAsBpD,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;YAKhD,UAAU;YAgCV,eAAe;CAqC9B;AAED,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,UAAU,EAChB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,eAAe,CAAC,CAI1B"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Transport layer cipher that combines AES-CTR and HMAC-SHA256.
3
+ * Manages encryption/decryption state, sequence numbers, and MAC verification.
4
+ */
5
+ import { encryptAES128CTR, decryptAES128CTR } from "../crypto/cipher";
6
+ import { computeHMACSHA256, constantTimeEqual } from "../crypto/mac";
7
+ import { buildPacket } from "../protocol/codec";
8
+ import { writeUint32, concat } from "../protocol/serialization";
9
+ import { HMAC_SHA256_SIZE, AES_BLOCK_SIZE, MAX_PACKET_SIZE } from "../domain/constants";
10
+ import { MacVerificationError, ProtocolError } from "../domain/errors";
11
+ export class TransportCipher {
12
+ constructor(ivC, keyC, macC, ivS, keyS, macS, initialSeqOut, initialSeqIn, macEtm, keyEncCrypto, keyDecCrypto) {
13
+ this.ivEnc = new Uint8Array(ivC);
14
+ this.ivDec = new Uint8Array(ivS);
15
+ this.keyEnc = keyEncCrypto;
16
+ this.keyDec = keyDecCrypto;
17
+ this.seqOut = initialSeqOut;
18
+ this.seqIn = initialSeqIn;
19
+ this.macEtm = macEtm;
20
+ this.macC = macC;
21
+ this.macS = macS;
22
+ }
23
+ async encrypt(payload) {
24
+ const raw = buildPacket(payload, this.macEtm);
25
+ const seq = this.seqOut++;
26
+ let ciphertext;
27
+ let macInput;
28
+ if (this.macEtm) {
29
+ const packetLen = raw.subarray(0, 4);
30
+ const { ciphertext: ct, nextIv } = await encryptAES128CTR(this.keyEnc, this.ivEnc, raw.subarray(4));
31
+ this.ivEnc = nextIv;
32
+ ciphertext = ct;
33
+ macInput = concat(writeUint32(seq), packetLen, ciphertext);
34
+ const mac = await computeHMACSHA256(this.macC, macInput);
35
+ return { ciphertext: concat(packetLen, ciphertext, mac) };
36
+ }
37
+ const { ciphertext: ct, nextIv } = await encryptAES128CTR(this.keyEnc, this.ivEnc, raw);
38
+ this.ivEnc = nextIv;
39
+ ciphertext = ct;
40
+ macInput = concat(writeUint32(seq), raw);
41
+ const mac = await computeHMACSHA256(this.macC, macInput);
42
+ return { ciphertext: concat(ciphertext, mac) };
43
+ }
44
+ async decrypt(data) {
45
+ if (this.macEtm)
46
+ return this.decryptEtm(data);
47
+ return this.decryptStandard(data);
48
+ }
49
+ async decryptEtm(data) {
50
+ if (data.length < 4 + HMAC_SHA256_SIZE)
51
+ return null;
52
+ const plen = new DataView(data.buffer, data.byteOffset, 4).getUint32(0, false);
53
+ if (plen < 5 || plen > MAX_PACKET_SIZE)
54
+ return null;
55
+ const totalLen = 4 + plen + HMAC_SHA256_SIZE;
56
+ if (data.length < totalLen)
57
+ return null;
58
+ const packetLen = data.subarray(0, 4);
59
+ const ciphertext = data.subarray(4, 4 + plen);
60
+ const macReceived = data.subarray(4 + plen, 4 + plen + HMAC_SHA256_SIZE);
61
+ const macInput = concat(writeUint32(this.seqIn), packetLen, ciphertext);
62
+ const macExpected = await computeHMACSHA256(this.macS, macInput);
63
+ if (!constantTimeEqual(macReceived, macExpected)) {
64
+ throw new MacVerificationError("MAC verification failed (EtM mode)");
65
+ }
66
+ const { plaintext: fullRawInner, nextIv } = await decryptAES128CTR(this.keyDec, this.ivDec, ciphertext);
67
+ const padLen = fullRawInner[0];
68
+ if (padLen < 4 || padLen > 255) {
69
+ throw new ProtocolError(`Invalid padding length: ${padLen} (seq=${this.seqIn})`);
70
+ }
71
+ if (padLen > plen - 1) {
72
+ throw new ProtocolError(`Padding exceeds packet length (seq=${this.seqIn})`);
73
+ }
74
+ const payloadLen = plen - 1 - padLen;
75
+ if (payloadLen < 0 || 1 + payloadLen > fullRawInner.length) {
76
+ throw new ProtocolError(`Invalid payload length: ${payloadLen} (seq=${this.seqIn})`);
77
+ }
78
+ this.ivDec = nextIv;
79
+ this.seqIn++;
80
+ const payload = fullRawInner.subarray(1, 1 + payloadLen);
81
+ return { payload, consumed: totalLen };
82
+ }
83
+ async decryptStandard(data) {
84
+ if (data.length < AES_BLOCK_SIZE + HMAC_SHA256_SIZE)
85
+ return null;
86
+ const firstBlock = data.subarray(0, AES_BLOCK_SIZE);
87
+ const { plaintext: first, nextIv: ivAfterFirst } = await decryptAES128CTR(this.keyDec, this.ivDec, firstBlock);
88
+ const plen = new DataView(first.buffer, first.byteOffset, 4).getUint32(0, false);
89
+ if (plen < 5 || plen > MAX_PACKET_SIZE)
90
+ return null;
91
+ const totalEnc = 4 + plen;
92
+ if (data.length < totalEnc + HMAC_SHA256_SIZE)
93
+ return null;
94
+ const macReceived = data.subarray(totalEnc, totalEnc + HMAC_SHA256_SIZE);
95
+ let fullRaw;
96
+ let ivAfterDecrypt;
97
+ if (totalEnc <= AES_BLOCK_SIZE) {
98
+ fullRaw = first.subarray(0, totalEnc);
99
+ ivAfterDecrypt = ivAfterFirst;
100
+ }
101
+ else {
102
+ const { plaintext: raw, nextIv: ivAfterRest } = await decryptAES128CTR(this.keyDec, ivAfterFirst, data.subarray(AES_BLOCK_SIZE, totalEnc));
103
+ ivAfterDecrypt = ivAfterRest;
104
+ fullRaw = concat(first, raw);
105
+ }
106
+ const padLen = fullRaw[4];
107
+ if (padLen < 4 || padLen > 255) {
108
+ throw new ProtocolError(`Invalid padding length: ${padLen} (seq=${this.seqIn})`);
109
+ }
110
+ const payloadLen = plen - 1 - padLen;
111
+ if (payloadLen < 0 || 5 + payloadLen > fullRaw.length) {
112
+ throw new ProtocolError(`Invalid payload length: ${payloadLen} (seq=${this.seqIn})`);
113
+ }
114
+ const macInput = concat(writeUint32(this.seqIn), fullRaw);
115
+ const macExpected = await computeHMACSHA256(this.macS, macInput);
116
+ if (!constantTimeEqual(macReceived, macExpected)) {
117
+ throw new MacVerificationError("MAC verification failed");
118
+ }
119
+ this.seqIn++;
120
+ this.ivDec = ivAfterDecrypt;
121
+ const payload = fullRaw.subarray(5, 5 + payloadLen);
122
+ return { payload, consumed: totalEnc + HMAC_SHA256_SIZE };
123
+ }
124
+ }
125
+ export async function createTransportCipher(ivC, keyC, macC, ivS, keyS, macS, initialSeqOut, initialSeqIn, macEtm) {
126
+ const keyEncCrypto = await crypto.subtle.importKey("raw", keyC, { name: "AES-CTR" }, false, ["encrypt"]);
127
+ const keyDecCrypto = await crypto.subtle.importKey("raw", keyS, { name: "AES-CTR" }, false, ["decrypt"]);
128
+ return new TransportCipher(ivC, keyC, macC, ivS, keyS, macS, initialSeqOut, initialSeqIn, macEtm, keyEncCrypto, keyDecCrypto);
129
+ }
@@ -0,0 +1,3 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
3
+ //# sourceMappingURL=vitest.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":";AAEA,wBAMG"}
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+ export default defineConfig({
3
+ test: {
4
+ include: ["**/*.test.ts"],
5
+ environment: "node",
6
+ globals: true,
7
+ },
8
+ });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "ssh2web",
3
+ "author": "Qasim Soomro",
4
+ "version": "1.0.0",
5
+ "description": "SSH-2 client for the browser over WebSocket. Key exchange, public-key auth, session API.",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "test": "vitest run",
23
+ "test:coverage": "vitest run --coverage",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "ssh",
28
+ "ssh2",
29
+ "websocket",
30
+ "browser",
31
+ "client"
32
+ ],
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/qas/ssh2web"
37
+ },
38
+ "devDependencies": {
39
+ "@vitest/coverage-v8": "^4.0.18",
40
+ "typescript": "5.9.3",
41
+ "vitest": "^4.0.18"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ }
46
+ }