gitspace 0.2.0-rc.1
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/.claude/settings.local.json +21 -0
- package/.gitspace/bundle.json +50 -0
- package/.gitspace/select/01-status.sh +40 -0
- package/.gitspace/setup/01-install-deps.sh +12 -0
- package/.gitspace/setup/02-typecheck.sh +16 -0
- package/AGENTS.md +439 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +25 -0
- package/README.md +607 -0
- package/bin/gssh +62 -0
- package/bun.lock +647 -0
- package/docs/CONNECTION.md +623 -0
- package/docs/GATEWAY-WORKER.md +319 -0
- package/docs/GETTING-STARTED.md +448 -0
- package/docs/GITSPACE-PLATFORM.md +1819 -0
- package/docs/INFRASTRUCTURE.md +1347 -0
- package/docs/PROTOCOL.md +619 -0
- package/docs/QUICKSTART.md +174 -0
- package/docs/RELAY.md +327 -0
- package/docs/REMOTE-DESIGN.md +549 -0
- package/docs/ROADMAP.md +564 -0
- package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
- package/docs/STACK-DESIGN.md +588 -0
- package/docs/UNIFIED_ARCHITECTURE.md +292 -0
- package/experiments/pty-benchmark.ts +148 -0
- package/experiments/pty-latency.ts +100 -0
- package/experiments/router/client.ts +199 -0
- package/experiments/router/protocol.ts +74 -0
- package/experiments/router/router.ts +217 -0
- package/experiments/router/session.ts +180 -0
- package/experiments/router/test.ts +133 -0
- package/experiments/socket-bandwidth.ts +77 -0
- package/homebrew/gitspace.rb +45 -0
- package/landing-page/ATTRIBUTIONS.md +3 -0
- package/landing-page/README.md +11 -0
- package/landing-page/bun.lock +801 -0
- package/landing-page/guidelines/Guidelines.md +61 -0
- package/landing-page/index.html +37 -0
- package/landing-page/package.json +90 -0
- package/landing-page/postcss.config.mjs +15 -0
- package/landing-page/public/_redirects +1 -0
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +53 -0
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
- package/landing-page/src/app/components/ui/accordion.tsx +66 -0
- package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
- package/landing-page/src/app/components/ui/alert.tsx +66 -0
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
- package/landing-page/src/app/components/ui/avatar.tsx +53 -0
- package/landing-page/src/app/components/ui/badge.tsx +46 -0
- package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
- package/landing-page/src/app/components/ui/button.tsx +57 -0
- package/landing-page/src/app/components/ui/calendar.tsx +75 -0
- package/landing-page/src/app/components/ui/card.tsx +92 -0
- package/landing-page/src/app/components/ui/carousel.tsx +241 -0
- package/landing-page/src/app/components/ui/chart.tsx +353 -0
- package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
- package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
- package/landing-page/src/app/components/ui/command.tsx +177 -0
- package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
- package/landing-page/src/app/components/ui/dialog.tsx +135 -0
- package/landing-page/src/app/components/ui/drawer.tsx +132 -0
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
- package/landing-page/src/app/components/ui/form.tsx +168 -0
- package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
- package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
- package/landing-page/src/app/components/ui/input.tsx +21 -0
- package/landing-page/src/app/components/ui/label.tsx +24 -0
- package/landing-page/src/app/components/ui/menubar.tsx +276 -0
- package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
- package/landing-page/src/app/components/ui/pagination.tsx +127 -0
- package/landing-page/src/app/components/ui/popover.tsx +48 -0
- package/landing-page/src/app/components/ui/progress.tsx +31 -0
- package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
- package/landing-page/src/app/components/ui/resizable.tsx +56 -0
- package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
- package/landing-page/src/app/components/ui/select.tsx +189 -0
- package/landing-page/src/app/components/ui/separator.tsx +28 -0
- package/landing-page/src/app/components/ui/sheet.tsx +139 -0
- package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
- package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
- package/landing-page/src/app/components/ui/slider.tsx +63 -0
- package/landing-page/src/app/components/ui/sonner.tsx +25 -0
- package/landing-page/src/app/components/ui/switch.tsx +31 -0
- package/landing-page/src/app/components/ui/table.tsx +116 -0
- package/landing-page/src/app/components/ui/tabs.tsx +66 -0
- package/landing-page/src/app/components/ui/textarea.tsx +18 -0
- package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
- package/landing-page/src/app/components/ui/toggle.tsx +47 -0
- package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
- package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
- package/landing-page/src/app/components/ui/utils.ts +6 -0
- package/landing-page/src/components/docs/DocsContent.tsx +718 -0
- package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
- package/landing-page/src/components/landing/CTA.tsx +59 -0
- package/landing-page/src/components/landing/Comparison.tsx +84 -0
- package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
- package/landing-page/src/components/landing/Features.tsx +201 -0
- package/landing-page/src/components/landing/Hero.tsx +142 -0
- package/landing-page/src/components/landing/Pricing.tsx +140 -0
- package/landing-page/src/components/landing/Roadmap.tsx +86 -0
- package/landing-page/src/components/landing/Security.tsx +81 -0
- package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
- package/landing-page/src/components/landing/UseCases.tsx +55 -0
- package/landing-page/src/components/landing/Workflow.tsx +101 -0
- package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
- package/landing-page/src/components/layout/Footer.tsx +55 -0
- package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
- package/landing-page/src/components/ui/badge.tsx +39 -0
- package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
- package/landing-page/src/components/ui/button.tsx +57 -0
- package/landing-page/src/components/ui/card.tsx +79 -0
- package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
- package/landing-page/src/components/ui/separator.tsx +28 -0
- package/landing-page/src/lib/utils.ts +6 -0
- package/landing-page/src/main.tsx +10 -0
- package/landing-page/src/pages/Dashboard.tsx +133 -0
- package/landing-page/src/pages/DocsPage.tsx +79 -0
- package/landing-page/src/pages/LandingPage.tsx +31 -0
- package/landing-page/src/pages/TerminalView.tsx +106 -0
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +3 -0
- package/landing-page/src/styles/tailwind.css +4 -0
- package/landing-page/src/styles/theme.css +181 -0
- package/landing-page/vite.config.ts +19 -0
- package/npm/darwin-arm64/bin/gssh +0 -0
- package/npm/darwin-arm64/package.json +20 -0
- package/package.json +74 -0
- package/scripts/build.ts +284 -0
- package/scripts/release.ts +140 -0
- package/src/__tests__/test-utils.ts +298 -0
- package/src/commands/__tests__/serve-messages.test.ts +190 -0
- package/src/commands/access.ts +298 -0
- package/src/commands/add.ts +452 -0
- package/src/commands/auth.ts +364 -0
- package/src/commands/connect.ts +287 -0
- package/src/commands/directory.ts +16 -0
- package/src/commands/host.ts +396 -0
- package/src/commands/identity.ts +184 -0
- package/src/commands/list.ts +200 -0
- package/src/commands/relay.ts +315 -0
- package/src/commands/remove.ts +241 -0
- package/src/commands/serve.ts +1493 -0
- package/src/commands/share.ts +456 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/switch.ts +353 -0
- package/src/commands/tmux.ts +317 -0
- package/src/core/__tests__/access.test.ts +240 -0
- package/src/core/access.ts +277 -0
- package/src/core/bundle.ts +342 -0
- package/src/core/config.ts +510 -0
- package/src/core/git.ts +317 -0
- package/src/core/github.ts +151 -0
- package/src/core/identity.ts +631 -0
- package/src/core/linear.ts +225 -0
- package/src/core/shell.ts +161 -0
- package/src/core/trusted-relays.ts +315 -0
- package/src/index.ts +821 -0
- package/src/lib/remote-session/index.ts +7 -0
- package/src/lib/remote-session/protocol.ts +267 -0
- package/src/lib/remote-session/session-handler.ts +581 -0
- package/src/lib/remote-session/workspace-scanner.ts +167 -0
- package/src/lib/tmux-lite/README.md +81 -0
- package/src/lib/tmux-lite/cli.ts +796 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
- package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
- package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
- package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
- package/src/lib/tmux-lite/crypto/frames.ts +141 -0
- package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
- package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
- package/src/lib/tmux-lite/crypto/identity.ts +286 -0
- package/src/lib/tmux-lite/crypto/index.ts +51 -0
- package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
- package/src/lib/tmux-lite/crypto/invites.ts +215 -0
- package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
- package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
- package/src/lib/tmux-lite/crypto/keys.ts +47 -0
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
- package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
- package/src/lib/tmux-lite/handshake-handler.ts +451 -0
- package/src/lib/tmux-lite/protocol.test.ts +307 -0
- package/src/lib/tmux-lite/protocol.ts +266 -0
- package/src/lib/tmux-lite/relay-client.ts +506 -0
- package/src/lib/tmux-lite/server.ts +1250 -0
- package/src/lib/tmux-lite/shell-integration.sh +37 -0
- package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
- package/src/lib/tmux-lite/terminal-queries.ts +49 -0
- package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
- package/src/relay/__tests__/helpers/auth.ts +354 -0
- package/src/relay/__tests__/helpers/ports.ts +51 -0
- package/src/relay/__tests__/protocol-validation.test.ts +265 -0
- package/src/relay/authorization.ts +303 -0
- package/src/relay/embedded-assets.generated.d.ts +15 -0
- package/src/relay/identity.ts +352 -0
- package/src/relay/index.ts +57 -0
- package/src/relay/pipes.test.ts +427 -0
- package/src/relay/pipes.ts +195 -0
- package/src/relay/protocol.ts +804 -0
- package/src/relay/registries.test.ts +437 -0
- package/src/relay/registries.ts +593 -0
- package/src/relay/server.test.ts +1323 -0
- package/src/relay/server.ts +1092 -0
- package/src/relay/signing.ts +238 -0
- package/src/relay/types.ts +69 -0
- package/src/serve/client-session-manager.ts +622 -0
- package/src/serve/daemon.ts +497 -0
- package/src/serve/pty-session.ts +236 -0
- package/src/serve/types.ts +169 -0
- package/src/shared/components/Flow.tsx +453 -0
- package/src/shared/components/Flow.tui.tsx +343 -0
- package/src/shared/components/Flow.web.tsx +442 -0
- package/src/shared/components/Inbox.tsx +446 -0
- package/src/shared/components/Inbox.tui.tsx +262 -0
- package/src/shared/components/Inbox.web.tsx +329 -0
- package/src/shared/components/MachineList.tsx +187 -0
- package/src/shared/components/MachineList.tui.tsx +161 -0
- package/src/shared/components/MachineList.web.tsx +210 -0
- package/src/shared/components/ProjectList.tsx +176 -0
- package/src/shared/components/ProjectList.tui.tsx +109 -0
- package/src/shared/components/ProjectList.web.tsx +143 -0
- package/src/shared/components/SpacesBrowser.tsx +332 -0
- package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
- package/src/shared/components/SpacesBrowser.web.tsx +221 -0
- package/src/shared/components/index.ts +103 -0
- package/src/shared/hooks/index.ts +16 -0
- package/src/shared/hooks/useNavigation.ts +226 -0
- package/src/shared/index.ts +122 -0
- package/src/shared/providers/LocalMachineProvider.ts +425 -0
- package/src/shared/providers/MachineProvider.ts +165 -0
- package/src/shared/providers/RemoteMachineProvider.ts +444 -0
- package/src/shared/providers/index.ts +26 -0
- package/src/shared/types.ts +145 -0
- package/src/tui/adapters.ts +120 -0
- package/src/tui/app.tsx +1816 -0
- package/src/tui/components/Terminal.tsx +580 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useAppState.ts +314 -0
- package/src/tui/hooks/useDaemonStatus.ts +174 -0
- package/src/tui/hooks/useInboxTUI.ts +113 -0
- package/src/tui/hooks/useRemoteMachines.ts +209 -0
- package/src/tui/index.ts +24 -0
- package/src/tui/state.ts +299 -0
- package/src/tui/terminal-bracketed-paste.test.ts +45 -0
- package/src/tui/terminal-bracketed-paste.ts +47 -0
- package/src/types/bundle.ts +112 -0
- package/src/types/config.ts +89 -0
- package/src/types/errors.ts +206 -0
- package/src/types/identity.ts +284 -0
- package/src/types/workspace-fuzzy.ts +49 -0
- package/src/types/workspace.ts +151 -0
- package/src/utils/bun-socket-writer.ts +80 -0
- package/src/utils/deps.ts +127 -0
- package/src/utils/fuzzy-match.ts +125 -0
- package/src/utils/logger.ts +127 -0
- package/src/utils/markdown.ts +254 -0
- package/src/utils/onboarding.ts +229 -0
- package/src/utils/prompts.ts +114 -0
- package/src/utils/run-commands.ts +112 -0
- package/src/utils/run-scripts.ts +142 -0
- package/src/utils/sanitize.ts +98 -0
- package/src/utils/secrets.ts +122 -0
- package/src/utils/shell-escape.ts +40 -0
- package/src/utils/utf8.ts +79 -0
- package/src/utils/workspace-state.ts +47 -0
- package/src/web/README.md +73 -0
- package/src/web/bun.lock +575 -0
- package/src/web/eslint.config.js +23 -0
- package/src/web/index.html +16 -0
- package/src/web/package.json +37 -0
- package/src/web/public/vite.svg +1 -0
- package/src/web/src/App.tsx +604 -0
- package/src/web/src/assets/react.svg +1 -0
- package/src/web/src/components/Terminal.tsx +207 -0
- package/src/web/src/hooks/useRelayConnection.ts +224 -0
- package/src/web/src/hooks/useTerminal.ts +699 -0
- package/src/web/src/index.css +55 -0
- package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
- package/src/web/src/lib/crypto/frames.ts +205 -0
- package/src/web/src/lib/crypto/handshake.ts +396 -0
- package/src/web/src/lib/crypto/identity.ts +128 -0
- package/src/web/src/lib/crypto/keyexchange.ts +246 -0
- package/src/web/src/lib/crypto/relay-signing.ts +53 -0
- package/src/web/src/lib/invite.ts +58 -0
- package/src/web/src/lib/storage/identity-store.ts +94 -0
- package/src/web/src/main.tsx +10 -0
- package/src/web/src/types/identity.ts +45 -0
- package/src/web/tsconfig.app.json +28 -0
- package/src/web/tsconfig.json +7 -0
- package/src/web/tsconfig.node.json +26 -0
- package/src/web/vite.config.ts +31 -0
- package/todo-security.md +92 -0
- package/tsconfig.json +23 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
- package/worker/bun.lock +237 -0
- package/worker/package.json +22 -0
- package/worker/schema.sql +96 -0
- package/worker/src/handlers/auth.ts +451 -0
- package/worker/src/handlers/subdomains.ts +376 -0
- package/worker/src/handlers/user.ts +98 -0
- package/worker/src/index.ts +70 -0
- package/worker/src/middleware/auth.ts +152 -0
- package/worker/src/services/cloudflare.ts +609 -0
- package/worker/src/types.ts +96 -0
- package/worker/tsconfig.json +15 -0
- package/worker/wrangler.toml +26 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for end-to-end encrypted communication
|
|
3
|
+
*
|
|
4
|
+
* Tests frame encryption/decryption after handshake establishment.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
createClientHello,
|
|
10
|
+
processServerHello,
|
|
11
|
+
createClientAuth,
|
|
12
|
+
} from "../../handshake.js";
|
|
13
|
+
import {
|
|
14
|
+
HandshakeHandler,
|
|
15
|
+
type EstablishedSession,
|
|
16
|
+
} from "../../../handshake-handler.js";
|
|
17
|
+
import { AccessControlList } from "../../access-control.js";
|
|
18
|
+
import { createFrame, openFrame, MASTER_STREAM_ID } from "../../index.js";
|
|
19
|
+
import {
|
|
20
|
+
createTestIdentityPair,
|
|
21
|
+
toPublicIdentity,
|
|
22
|
+
createIdentityFixtures,
|
|
23
|
+
} from "../helpers/test-identities.js";
|
|
24
|
+
import { runCompleteHandshake, verifyKeyPairing } from "../helpers/handshake-runner.js";
|
|
25
|
+
import type { X3DHResponseMessage, SessionKeys } from "../../../../../types/identity.js";
|
|
26
|
+
|
|
27
|
+
describe("E2E Communication Integration", () => {
|
|
28
|
+
describe("frame encryption after handshake", () => {
|
|
29
|
+
it("should encrypt and decrypt frames with session keys", async () => {
|
|
30
|
+
const { client, machine } = createTestIdentityPair();
|
|
31
|
+
const accessList = new AccessControlList();
|
|
32
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
33
|
+
|
|
34
|
+
const result = await runCompleteHandshake(
|
|
35
|
+
client,
|
|
36
|
+
machine,
|
|
37
|
+
accessList,
|
|
38
|
+
{ type: "access_list" }
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(result.success).toBe(true);
|
|
42
|
+
const clientKeys = result.clientKeys!;
|
|
43
|
+
const machineKeys = result.machineSession!.sessionKeys;
|
|
44
|
+
|
|
45
|
+
// Client encrypts with sendKey
|
|
46
|
+
const message = Buffer.from("Hello, machine!");
|
|
47
|
+
const encrypted = createFrame(
|
|
48
|
+
MASTER_STREAM_ID,
|
|
49
|
+
message,
|
|
50
|
+
Buffer.from(clientKeys.sendKey)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Machine decrypts with receiveKey (which equals client's sendKey)
|
|
54
|
+
const decrypted = openFrame(encrypted, Buffer.from(machineKeys.receiveKey));
|
|
55
|
+
|
|
56
|
+
expect(decrypted).not.toBeNull();
|
|
57
|
+
expect(decrypted!.streamId).toBe(MASTER_STREAM_ID);
|
|
58
|
+
expect(decrypted!.data.toString()).toBe("Hello, machine!");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should handle bidirectional communication", async () => {
|
|
62
|
+
const { client, machine } = createTestIdentityPair();
|
|
63
|
+
const accessList = new AccessControlList();
|
|
64
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
65
|
+
|
|
66
|
+
const result = await runCompleteHandshake(
|
|
67
|
+
client,
|
|
68
|
+
machine,
|
|
69
|
+
accessList,
|
|
70
|
+
{ type: "access_list" }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(result.success).toBe(true);
|
|
74
|
+
const clientKeys = result.clientKeys!;
|
|
75
|
+
const machineKeys = result.machineSession!.sessionKeys;
|
|
76
|
+
|
|
77
|
+
// Client -> Machine
|
|
78
|
+
const clientMsg = Buffer.from("Request from client");
|
|
79
|
+
const encryptedReq = createFrame(
|
|
80
|
+
MASTER_STREAM_ID,
|
|
81
|
+
clientMsg,
|
|
82
|
+
Buffer.from(clientKeys.sendKey)
|
|
83
|
+
);
|
|
84
|
+
const decryptedReq = openFrame(
|
|
85
|
+
encryptedReq,
|
|
86
|
+
Buffer.from(machineKeys.receiveKey)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(decryptedReq?.data.toString()).toBe("Request from client");
|
|
90
|
+
|
|
91
|
+
// Machine -> Client
|
|
92
|
+
const machineMsg = Buffer.from("Response from machine");
|
|
93
|
+
const encryptedRes = createFrame(
|
|
94
|
+
MASTER_STREAM_ID,
|
|
95
|
+
machineMsg,
|
|
96
|
+
Buffer.from(machineKeys.sendKey)
|
|
97
|
+
);
|
|
98
|
+
const decryptedRes = openFrame(
|
|
99
|
+
encryptedRes,
|
|
100
|
+
Buffer.from(clientKeys.receiveKey)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(decryptedRes?.data.toString()).toBe("Response from machine");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should fail to decrypt with wrong key", async () => {
|
|
107
|
+
const { client, machine } = createTestIdentityPair();
|
|
108
|
+
const accessList = new AccessControlList();
|
|
109
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
110
|
+
|
|
111
|
+
const result = await runCompleteHandshake(
|
|
112
|
+
client,
|
|
113
|
+
machine,
|
|
114
|
+
accessList,
|
|
115
|
+
{ type: "access_list" }
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(result.success).toBe(true);
|
|
119
|
+
const clientKeys = result.clientKeys!;
|
|
120
|
+
|
|
121
|
+
// Encrypt with sendKey
|
|
122
|
+
const message = Buffer.from("Secret message");
|
|
123
|
+
const encrypted = createFrame(
|
|
124
|
+
MASTER_STREAM_ID,
|
|
125
|
+
message,
|
|
126
|
+
Buffer.from(clientKeys.sendKey)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Try to decrypt with wrong key (receiveKey instead of sendKey)
|
|
130
|
+
const wrongDecrypt = openFrame(
|
|
131
|
+
encrypted,
|
|
132
|
+
Buffer.from(clientKeys.receiveKey)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(wrongDecrypt).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should fail to decrypt with random key", async () => {
|
|
139
|
+
const { client, machine } = createTestIdentityPair();
|
|
140
|
+
const accessList = new AccessControlList();
|
|
141
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
142
|
+
|
|
143
|
+
const result = await runCompleteHandshake(
|
|
144
|
+
client,
|
|
145
|
+
machine,
|
|
146
|
+
accessList,
|
|
147
|
+
{ type: "access_list" }
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(result.success).toBe(true);
|
|
151
|
+
const clientKeys = result.clientKeys!;
|
|
152
|
+
|
|
153
|
+
// Encrypt normally
|
|
154
|
+
const message = Buffer.from("Secret message");
|
|
155
|
+
const encrypted = createFrame(
|
|
156
|
+
MASTER_STREAM_ID,
|
|
157
|
+
message,
|
|
158
|
+
Buffer.from(clientKeys.sendKey)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Try to decrypt with random key
|
|
162
|
+
const randomKey = Buffer.alloc(32);
|
|
163
|
+
for (let i = 0; i < 32; i++) {
|
|
164
|
+
randomKey[i] = Math.floor(Math.random() * 256);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const wrongDecrypt = openFrame(encrypted, randomKey);
|
|
168
|
+
expect(wrongDecrypt).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("stream IDs", () => {
|
|
173
|
+
it("should handle different stream IDs", async () => {
|
|
174
|
+
const { client, machine } = createTestIdentityPair();
|
|
175
|
+
const accessList = new AccessControlList();
|
|
176
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
177
|
+
|
|
178
|
+
const result = await runCompleteHandshake(
|
|
179
|
+
client,
|
|
180
|
+
machine,
|
|
181
|
+
accessList,
|
|
182
|
+
{ type: "access_list" }
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(result.success).toBe(true);
|
|
186
|
+
const clientKeys = result.clientKeys!;
|
|
187
|
+
const machineKeys = result.machineSession!.sessionKeys;
|
|
188
|
+
|
|
189
|
+
// Send on different streams
|
|
190
|
+
const stream0 = createFrame(0, Buffer.from("stream 0"), Buffer.from(clientKeys.sendKey));
|
|
191
|
+
const stream1 = createFrame(1, Buffer.from("stream 1"), Buffer.from(clientKeys.sendKey));
|
|
192
|
+
const stream255 = createFrame(255, Buffer.from("stream 255"), Buffer.from(clientKeys.sendKey));
|
|
193
|
+
|
|
194
|
+
const dec0 = openFrame(stream0, Buffer.from(machineKeys.receiveKey));
|
|
195
|
+
const dec1 = openFrame(stream1, Buffer.from(machineKeys.receiveKey));
|
|
196
|
+
const dec255 = openFrame(stream255, Buffer.from(machineKeys.receiveKey));
|
|
197
|
+
|
|
198
|
+
expect(dec0?.streamId).toBe(0);
|
|
199
|
+
expect(dec0?.data.toString()).toBe("stream 0");
|
|
200
|
+
|
|
201
|
+
expect(dec1?.streamId).toBe(1);
|
|
202
|
+
expect(dec1?.data.toString()).toBe("stream 1");
|
|
203
|
+
|
|
204
|
+
expect(dec255?.streamId).toBe(255);
|
|
205
|
+
expect(dec255?.data.toString()).toBe("stream 255");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("message integrity", () => {
|
|
210
|
+
it("should detect tampered ciphertext", async () => {
|
|
211
|
+
const { client, machine } = createTestIdentityPair();
|
|
212
|
+
const accessList = new AccessControlList();
|
|
213
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
214
|
+
|
|
215
|
+
const result = await runCompleteHandshake(
|
|
216
|
+
client,
|
|
217
|
+
machine,
|
|
218
|
+
accessList,
|
|
219
|
+
{ type: "access_list" }
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect(result.success).toBe(true);
|
|
223
|
+
const clientKeys = result.clientKeys!;
|
|
224
|
+
const machineKeys = result.machineSession!.sessionKeys;
|
|
225
|
+
|
|
226
|
+
// Encrypt a message
|
|
227
|
+
const message = Buffer.from("Sensitive data");
|
|
228
|
+
const encrypted = createFrame(
|
|
229
|
+
MASTER_STREAM_ID,
|
|
230
|
+
message,
|
|
231
|
+
Buffer.from(clientKeys.sendKey)
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Tamper with the ciphertext
|
|
235
|
+
const tampered = Buffer.from(encrypted);
|
|
236
|
+
tampered[10] ^= 0xff; // Flip bits in the middle
|
|
237
|
+
|
|
238
|
+
// Should fail authentication
|
|
239
|
+
const decrypted = openFrame(tampered, Buffer.from(machineKeys.receiveKey));
|
|
240
|
+
expect(decrypted).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should detect truncated message", async () => {
|
|
244
|
+
const { client, machine } = createTestIdentityPair();
|
|
245
|
+
const accessList = new AccessControlList();
|
|
246
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
247
|
+
|
|
248
|
+
const result = await runCompleteHandshake(
|
|
249
|
+
client,
|
|
250
|
+
machine,
|
|
251
|
+
accessList,
|
|
252
|
+
{ type: "access_list" }
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(result.success).toBe(true);
|
|
256
|
+
const clientKeys = result.clientKeys!;
|
|
257
|
+
const machineKeys = result.machineSession!.sessionKeys;
|
|
258
|
+
|
|
259
|
+
// Encrypt a message
|
|
260
|
+
const message = Buffer.from("Complete message");
|
|
261
|
+
const encrypted = createFrame(
|
|
262
|
+
MASTER_STREAM_ID,
|
|
263
|
+
message,
|
|
264
|
+
Buffer.from(clientKeys.sendKey)
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Truncate the message
|
|
268
|
+
const truncated = encrypted.subarray(0, encrypted.length - 10);
|
|
269
|
+
|
|
270
|
+
// Should fail
|
|
271
|
+
const decrypted = openFrame(truncated, Buffer.from(machineKeys.receiveKey));
|
|
272
|
+
expect(decrypted).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("session isolation", () => {
|
|
277
|
+
it("should not decrypt messages from different sessions", async () => {
|
|
278
|
+
const fixtures = createIdentityFixtures();
|
|
279
|
+
const accessList = new AccessControlList();
|
|
280
|
+
accessList.addEntry(fixtures.alicePublic);
|
|
281
|
+
accessList.addEntry(fixtures.bobPublic);
|
|
282
|
+
|
|
283
|
+
// Alice's session
|
|
284
|
+
const aliceResult = await runCompleteHandshake(
|
|
285
|
+
fixtures.alice,
|
|
286
|
+
fixtures.machine,
|
|
287
|
+
accessList,
|
|
288
|
+
{ type: "access_list" }
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Bob's session
|
|
292
|
+
const bobResult = await runCompleteHandshake(
|
|
293
|
+
fixtures.bob,
|
|
294
|
+
fixtures.machine,
|
|
295
|
+
accessList,
|
|
296
|
+
{ type: "access_list" }
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
expect(aliceResult.success).toBe(true);
|
|
300
|
+
expect(bobResult.success).toBe(true);
|
|
301
|
+
|
|
302
|
+
const aliceKeys = aliceResult.clientKeys!;
|
|
303
|
+
const bobKeys = bobResult.clientKeys!;
|
|
304
|
+
|
|
305
|
+
// Alice encrypts a message
|
|
306
|
+
const aliceMsg = Buffer.from("Alice's secret");
|
|
307
|
+
const aliceEncrypted = createFrame(
|
|
308
|
+
MASTER_STREAM_ID,
|
|
309
|
+
aliceMsg,
|
|
310
|
+
Buffer.from(aliceKeys.sendKey)
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Bob should NOT be able to decrypt Alice's message
|
|
314
|
+
const bobAttempt = openFrame(
|
|
315
|
+
aliceEncrypted,
|
|
316
|
+
Buffer.from(bobKeys.receiveKey)
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(bobAttempt).toBeNull();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should isolate keys between repeated handshakes", async () => {
|
|
323
|
+
const { client, machine } = createTestIdentityPair();
|
|
324
|
+
const accessList = new AccessControlList();
|
|
325
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
326
|
+
|
|
327
|
+
// First session
|
|
328
|
+
const result1 = await runCompleteHandshake(
|
|
329
|
+
client,
|
|
330
|
+
machine,
|
|
331
|
+
accessList,
|
|
332
|
+
{ type: "access_list" }
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Second session
|
|
336
|
+
const result2 = await runCompleteHandshake(
|
|
337
|
+
client,
|
|
338
|
+
machine,
|
|
339
|
+
accessList,
|
|
340
|
+
{ type: "access_list" }
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
expect(result1.success).toBe(true);
|
|
344
|
+
expect(result2.success).toBe(true);
|
|
345
|
+
|
|
346
|
+
const keys1 = result1.clientKeys!;
|
|
347
|
+
const keys2 = result2.clientKeys!;
|
|
348
|
+
|
|
349
|
+
// Encrypt with first session's keys
|
|
350
|
+
const message = Buffer.from("Session 1 message");
|
|
351
|
+
const encrypted = createFrame(
|
|
352
|
+
MASTER_STREAM_ID,
|
|
353
|
+
message,
|
|
354
|
+
Buffer.from(keys1.sendKey)
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// Should NOT decrypt with second session's keys
|
|
358
|
+
const wrongSession = openFrame(
|
|
359
|
+
encrypted,
|
|
360
|
+
Buffer.from(keys2.receiveKey)
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
expect(wrongSession).toBeNull();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("large messages", () => {
|
|
368
|
+
it("should handle large payloads", async () => {
|
|
369
|
+
const { client, machine } = createTestIdentityPair();
|
|
370
|
+
const accessList = new AccessControlList();
|
|
371
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
372
|
+
|
|
373
|
+
const result = await runCompleteHandshake(
|
|
374
|
+
client,
|
|
375
|
+
machine,
|
|
376
|
+
accessList,
|
|
377
|
+
{ type: "access_list" }
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(result.success).toBe(true);
|
|
381
|
+
const clientKeys = result.clientKeys!;
|
|
382
|
+
const machineKeys = result.machineSession!.sessionKeys;
|
|
383
|
+
|
|
384
|
+
// Create a large message (64KB)
|
|
385
|
+
const largeMessage = Buffer.alloc(64 * 1024);
|
|
386
|
+
for (let i = 0; i < largeMessage.length; i++) {
|
|
387
|
+
largeMessage[i] = i % 256;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const encrypted = createFrame(
|
|
391
|
+
MASTER_STREAM_ID,
|
|
392
|
+
largeMessage,
|
|
393
|
+
Buffer.from(clientKeys.sendKey)
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const decrypted = openFrame(
|
|
397
|
+
encrypted,
|
|
398
|
+
Buffer.from(machineKeys.receiveKey)
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
expect(decrypted).not.toBeNull();
|
|
402
|
+
expect(decrypted!.data.length).toBe(largeMessage.length);
|
|
403
|
+
expect(Buffer.compare(decrypted!.data, largeMessage)).toBe(0);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("should handle empty messages", async () => {
|
|
407
|
+
const { client, machine } = createTestIdentityPair();
|
|
408
|
+
const accessList = new AccessControlList();
|
|
409
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
410
|
+
|
|
411
|
+
const result = await runCompleteHandshake(
|
|
412
|
+
client,
|
|
413
|
+
machine,
|
|
414
|
+
accessList,
|
|
415
|
+
{ type: "access_list" }
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect(result.success).toBe(true);
|
|
419
|
+
const clientKeys = result.clientKeys!;
|
|
420
|
+
const machineKeys = result.machineSession!.sessionKeys;
|
|
421
|
+
|
|
422
|
+
// Empty message
|
|
423
|
+
const emptyMessage = Buffer.alloc(0);
|
|
424
|
+
|
|
425
|
+
const encrypted = createFrame(
|
|
426
|
+
MASTER_STREAM_ID,
|
|
427
|
+
emptyMessage,
|
|
428
|
+
Buffer.from(clientKeys.sendKey)
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const decrypted = openFrame(
|
|
432
|
+
encrypted,
|
|
433
|
+
Buffer.from(machineKeys.receiveKey)
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
expect(decrypted).not.toBeNull();
|
|
437
|
+
expect(decrypted!.data.length).toBe(0);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe("multiple messages", () => {
|
|
442
|
+
it("should handle many messages in sequence", async () => {
|
|
443
|
+
const { client, machine } = createTestIdentityPair();
|
|
444
|
+
const accessList = new AccessControlList();
|
|
445
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
446
|
+
|
|
447
|
+
const result = await runCompleteHandshake(
|
|
448
|
+
client,
|
|
449
|
+
machine,
|
|
450
|
+
accessList,
|
|
451
|
+
{ type: "access_list" }
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
expect(result.success).toBe(true);
|
|
455
|
+
const clientKeys = result.clientKeys!;
|
|
456
|
+
const machineKeys = result.machineSession!.sessionKeys;
|
|
457
|
+
|
|
458
|
+
// Send 100 messages
|
|
459
|
+
for (let i = 0; i < 100; i++) {
|
|
460
|
+
const message = Buffer.from(`Message ${i}`);
|
|
461
|
+
const encrypted = createFrame(
|
|
462
|
+
MASTER_STREAM_ID,
|
|
463
|
+
message,
|
|
464
|
+
Buffer.from(clientKeys.sendKey)
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const decrypted = openFrame(
|
|
468
|
+
encrypted,
|
|
469
|
+
Buffer.from(machineKeys.receiveKey)
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
expect(decrypted).not.toBeNull();
|
|
473
|
+
expect(decrypted!.data.toString()).toBe(`Message ${i}`);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|