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
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # SSH Client
2
+
3
+ ![Demo](static/img/demo.png)
4
+
5
+ SSH-2 client library for the browser (or any environment with Web Crypto and WebSockets). Connects over a WebSocket transport, performs key exchange, public-key auth, and exposes a session API for reading/writing and PTY control. This was built for [Agentripper.com](https://agentripper.com) because other solutions did not satisfy our requirements.
6
+
7
+ **Install:** `npm install ssh2web`
8
+ **Use in another project:** Point your bundler at the package or copy this directory; the package builds with `npm run build` (output in `dist/`).
9
+
10
+ ## Public API
11
+
12
+ ```typescript
13
+ import { connectSSH } from "lib/sshClient"; // or your path to the lib
14
+
15
+ const conn = await connectSSH(
16
+ ws, // WebSocket (binary)
17
+ { username, certificate, privateKey }, // credentials
18
+ onError, // (err: string) => void
19
+ { cols, rows, onPtyDenied } // optional: terminal size, pty callback
20
+ );
21
+
22
+ conn.onData(data => { /* handle server output */ });
23
+ conn.write(userInput);
24
+ conn.resize(cols, rows);
25
+ conn.close();
26
+ ```
27
+
28
+ Types: `SSHConnection`, `ConnectSSHOptions`, `SSHCredentials`. Errors: `SSHError`, `KEXError`, `AuthenticationError`, `MacVerificationError`, `ProtocolError`, `ChannelError`, `ParseError` (exported from the same module).
29
+
30
+ ## Architecture
31
+
32
+ | Layer | Purpose |
33
+ |-------|---------|
34
+ | **domain/** | Models, constants, errors |
35
+ | **crypto/** | Digest, cipher, MAC, DH/X25519, key derivation |
36
+ | **protocol/** | Packet codec, serialization, deserialization |
37
+ | **transport/** | AES-CTR + HMAC, sequence numbers |
38
+ | **kex/** | KEXINIT builder, KEX state |
39
+ | **auth/** | Auth state, cert parsing, PEM + Ed25519 sign |
40
+ | **channel/** | Channel state |
41
+ | **connection/** | Orchestrator and public types |
42
+
43
+ ## Directory layout
44
+
45
+ ```
46
+ sshClient/
47
+ index.ts (public API)
48
+ ARCHITECTURE.md
49
+ REFACTORING.md
50
+ domain/ constants, errors, models
51
+ crypto/ digest, cipher, mac, arithmetic, keyexchange, keys (+ *.test.ts)
52
+ protocol/ serialization, deserialization, codec, messages (+ *.test.ts)
53
+ transport/ transportcipher (+ *.test.ts)
54
+ kex/ kexstate, builder (+ *.test.ts)
55
+ auth/ authstate, certparser, keys (+ *.test.ts)
56
+ channel/ channelstate (+ *.test.ts)
57
+ connection/ connectSSH, connectionstate, types
58
+ ```
59
+
60
+ ## Testing
61
+
62
+ From the repo root that contains this lib (e.g. `website/`):
63
+
64
+ ```bash
65
+ npm run test
66
+ ```
67
+
68
+ With coverage:
69
+
70
+ ```bash
71
+ npm run test -- --coverage
72
+ ```
73
+
74
+ ### Coverage (Vitest, v8)
75
+
76
+ Latest run (82 tests, 19 files):
77
+
78
+ | Metric | Coverage |
79
+ |-----------|----------|
80
+ | Statements| 43.1% |
81
+ | Branches | 27.6% |
82
+ | Functions | 54.1% |
83
+ | Lines | 43.4% |
84
+
85
+ | Area | Stmts | Lines |
86
+ |------------|-------|-------|
87
+ | auth | 90.5 | 88.2 |
88
+ | crypto | 100 | 100 |
89
+ | domain | 76.8 | 84.1 |
90
+ | kex | 82.4 | 78.6 |
91
+ | protocol | 73.9 | 78.3 |
92
+ | transport | 84.4 | 89.2 |
93
+ | channel | 50 | 50 |
94
+ | connection | 1.0 | 1.1 (orchestrator largely integration) |
95
+
96
+ Unit tests are colocated (`*.test.ts`). The connection orchestrator is exercised by end-to-end use; crypto, protocol, transport, auth, and state layers are unit-tested.
97
+
98
+ ## Design principles
99
+
100
+ - **KISS** – Simple, direct implementations
101
+ - **Single responsibility** – One concern per module
102
+ - **Layered** – Domain to crypto to protocol to transport to connection
103
+ - **Test-friendly** – Layers testable in isolation
104
+
105
+ ## Data flow
106
+
107
+ ```
108
+ Caller
109
+ |
110
+ connectSSH(ws, creds) -> connection/connectSSH
111
+ - Version exchange (client/server ident)
112
+ - KEX (KEXINIT, KEXDH/KEX_ECDH, NEWKEYS)
113
+ - Key derivation, transport cipher
114
+ - Service request (ssh-userauth)
115
+ - USERAUTH (publickey, cert + signature)
116
+ - CHANNEL_OPEN session, pty-req, shell
117
+ - Returns SSHConnection
118
+ - write(data)
119
+ - onData(cb)
120
+ - resize(cols, rows)
121
+ - close()
122
+ ```
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Authentication state machine.
3
+ * Manages auth flow: service request → userauth → success/failure.
4
+ */
5
+ export type AuthPhase = "init" | "service_requested" | "awaiting_pk_ok" | "signed" | "complete" | "failed";
6
+ export interface AuthState {
7
+ phase: AuthPhase;
8
+ receivedPKOk: boolean;
9
+ error: string | null;
10
+ }
11
+ export declare function createAuthState(): AuthState;
12
+ export declare function transitionAuthPhase(current: AuthPhase): AuthPhase;
13
+ export declare function setAuthFailed(state: AuthState, error: string): AuthState;
14
+ //# sourceMappingURL=authstate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authstate.d.ts","sourceRoot":"","sources":["../../auth/authstate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,mBAAmB,GAAG,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE3G,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,SAAS,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,eAAe,IAAI,SAAS,CAM3C;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAUjE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAExE"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Authentication state machine.
3
+ * Manages auth flow: service request → userauth → success/failure.
4
+ */
5
+ export function createAuthState() {
6
+ return {
7
+ phase: "init",
8
+ receivedPKOk: false,
9
+ error: null,
10
+ };
11
+ }
12
+ export function transitionAuthPhase(current) {
13
+ const transitions = {
14
+ "init": "service_requested",
15
+ "service_requested": "awaiting_pk_ok",
16
+ "awaiting_pk_ok": "signed",
17
+ "signed": "complete",
18
+ "complete": "complete",
19
+ "failed": "failed",
20
+ };
21
+ return transitions[current];
22
+ }
23
+ export function setAuthFailed(state, error) {
24
+ return { ...state, phase: "failed", error };
25
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * SSH certificate parsing.
3
+ * Extracts key type and certificate blob from base64-encoded format.
4
+ */
5
+ export declare function parseCertBase64(cert: string): {
6
+ keyType: string;
7
+ certBlob: Uint8Array;
8
+ };
9
+ //# sourceMappingURL=certparser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"certparser.d.ts","sourceRoot":"","sources":["../../auth/certparser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,UAAU,CAAA;CAAE,CAOvF"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SSH certificate parsing.
3
+ * Extracts key type and certificate blob from base64-encoded format.
4
+ */
5
+ export function parseCertBase64(cert) {
6
+ const parts = cert.trim().replace(/\s+/g, " ").split(" ");
7
+ if (parts.length < 2)
8
+ throw new Error("Invalid certificate format");
9
+ const keyType = parts[0];
10
+ const b64 = parts[1].replace(/\s/g, "");
11
+ const certBlob = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
12
+ return { keyType, certBlob };
13
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Certificate and private key parsing.
3
+ * Extracts key material from PEM and base64-encoded SSH certs.
4
+ */
5
+ export declare function parsePemPrivateKey(pem: string): Promise<CryptoKey>;
6
+ export declare function ed25519Sign(privateKey: CryptoKey, data: Uint8Array): Promise<Uint8Array>;
7
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../auth/keys.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAMxE;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAK9F"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Certificate and private key parsing.
3
+ * Extracts key material from PEM and base64-encoded SSH certs.
4
+ */
5
+ export async function parsePemPrivateKey(pem) {
6
+ const m = pem.match(/-----BEGIN PRIVATE KEY-----([\s\S]*?)-----END PRIVATE KEY-----/);
7
+ if (!m)
8
+ throw new Error("Invalid PEM format");
9
+ const b64 = m[1].replace(/\s/g, "");
10
+ const der = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
11
+ return crypto.subtle.importKey("pkcs8", der, { name: "Ed25519" }, false, ["sign"]);
12
+ }
13
+ export async function ed25519Sign(privateKey, data) {
14
+ const buffer = data.byteOffset === 0 && data.byteLength === data.buffer.byteLength
15
+ ? data.buffer
16
+ : data.slice().buffer;
17
+ return new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, privateKey, buffer));
18
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Channel management state machine.
3
+ * Tracks channel lifecycle: open → pty_request → shell → data_exchange.
4
+ */
5
+ export type ChannelPhase = "init" | "opening" | "open" | "pty_requested" | "shell_requested" | "active" | "closed";
6
+ export interface ChannelState {
7
+ phase: ChannelPhase;
8
+ clientChannelId: number;
9
+ serverChannelId: number;
10
+ windowSizeLocal: number;
11
+ windowSizeRemote: number;
12
+ ptySent: boolean;
13
+ shellSent: boolean;
14
+ }
15
+ export declare function createChannelState(defaultWindowSize: number): ChannelState;
16
+ export declare function transitionChannelPhase(current: ChannelPhase): ChannelPhase;
17
+ export declare function adjustWindowSize(state: ChannelState, consumed: number): ChannelState;
18
+ //# sourceMappingURL=channelstate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channelstate.d.ts","sourceRoot":"","sources":["../../channel/channelstate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,eAAe,GAAG,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEnH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,YAAY,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,kBAAkB,CAAC,iBAAiB,EAAE,MAAM,GAAG,YAAY,CAU1E;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,CAW1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAEpF"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Channel management state machine.
3
+ * Tracks channel lifecycle: open → pty_request → shell → data_exchange.
4
+ */
5
+ export function createChannelState(defaultWindowSize) {
6
+ return {
7
+ phase: "init",
8
+ clientChannelId: 0,
9
+ serverChannelId: 0,
10
+ windowSizeLocal: defaultWindowSize,
11
+ windowSizeRemote: defaultWindowSize,
12
+ ptySent: false,
13
+ shellSent: false,
14
+ };
15
+ }
16
+ export function transitionChannelPhase(current) {
17
+ const transitions = {
18
+ "init": "opening",
19
+ "opening": "open",
20
+ "open": "pty_requested",
21
+ "pty_requested": "shell_requested",
22
+ "shell_requested": "active",
23
+ "active": "active",
24
+ "closed": "closed",
25
+ };
26
+ return transitions[current];
27
+ }
28
+ export function adjustWindowSize(state, consumed) {
29
+ return { ...state, windowSizeLocal: state.windowSizeLocal - consumed };
30
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Main SSH connection orchestrator.
3
+ * Entry point that coordinates all layers: domain, crypto, protocol, transport, state machines.
4
+ */
5
+ import type { SSHConnection, ConnectSSHOptions } from "./types";
6
+ import type { Credentials as SSHCredentials } from "../domain/models";
7
+ export declare function connectSSH(ws: WebSocket, creds: SSHCredentials, onError?: (err: string) => void, options?: ConnectSSHOptions): Promise<SSHConnection>;
8
+ //# sourceMappingURL=connectSSH.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectSSH.d.ts","sourceRoot":"","sources":["../../connection/connectSSH.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAoDtE,wBAAsB,UAAU,CAC9B,EAAE,EAAE,SAAS,EACb,KAAK,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAC/B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,aAAa,CAAC,CAkBxB"}