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
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# SSH Client
|
|
2
|
+
|
|
3
|
+

|
|
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 @@
|
|
|
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"}
|