@ozyman42/ozy-cli 0.4.8-linux-x64.0 → 0.4.8
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/bin/ozy +191 -0
- package/bin/ozy-signing-agent +191 -0
- package/bin/ozy-ssh-keygen +191 -0
- package/bin/ozy-virtual-security-key +191 -0
- package/dist/commands/git/hosts.js +18 -0
- package/dist/commands/git/hosts.js.map +1 -0
- package/dist/commands/git/index.js +8 -0
- package/dist/commands/git/index.js.map +1 -0
- package/dist/commands/git/setup.js +145 -0
- package/dist/commands/git/setup.js.map +1 -0
- package/dist/commands/index.js +13 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/npm/index.js +7 -0
- package/dist/commands/npm/index.js.map +1 -0
- package/dist/commands/npm/setup/git-remote.js +66 -0
- package/dist/commands/npm/setup/git-remote.js.map +1 -0
- package/dist/commands/npm/setup/index.js +168 -0
- package/dist/commands/npm/setup/index.js.map +1 -0
- package/dist/commands/npm/setup/npm-auth.js +62 -0
- package/dist/commands/npm/setup/npm-auth.js.map +1 -0
- package/dist/commands/npm/setup/package-json-validate.js +47 -0
- package/dist/commands/npm/setup/package-json-validate.js.map +1 -0
- package/dist/commands/npm/setup/package-registry.js +20 -0
- package/dist/commands/npm/setup/package-registry.js.map +1 -0
- package/dist/commands/npm/setup/setup.test.js +95 -0
- package/dist/commands/npm/setup/setup.test.js.map +1 -0
- package/dist/commands/npm/setup/trusted-publishing.js +84 -0
- package/dist/commands/npm/setup/trusted-publishing.js.map +1 -0
- package/dist/commands/npm/setup/workflow.js +19 -0
- package/dist/commands/npm/setup/workflow.js.map +1 -0
- package/dist/commands/ssh/get-sk-credential.js +104 -0
- package/dist/commands/ssh/get-sk-credential.js.map +1 -0
- package/dist/commands/ssh/index.js +7 -0
- package/dist/commands/ssh/index.js.map +1 -0
- package/dist/commands/upgrade.js +199 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/common/command.js +15 -0
- package/dist/common/command.js.map +1 -0
- package/dist/common/constants.js +25 -0
- package/dist/common/constants.js.map +1 -0
- package/dist/common/effective-modules-extensions.js +13 -0
- package/dist/common/effective-modules-extensions.js.map +1 -0
- package/dist/common/log.js +4 -0
- package/dist/common/log.js.map +1 -0
- package/dist/common/render-caller-tree.js +20 -0
- package/dist/common/render-caller-tree.js.map +1 -0
- package/dist/entrypoints/ozy-signing-agent.js +48 -0
- package/dist/entrypoints/ozy-signing-agent.js.map +1 -0
- package/dist/entrypoints/ozy-ssh-keygen.js +9 -0
- package/dist/entrypoints/ozy-ssh-keygen.js.map +1 -0
- package/dist/entrypoints/ozy-virtual-security-key.js +18 -0
- package/dist/entrypoints/ozy-virtual-security-key.js.map +1 -0
- package/dist/entrypoints/ozy.js +3 -0
- package/dist/entrypoints/ozy.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/thing.js +4 -0
- package/dist/internal/thing.js.map +1 -0
- package/dist/modules/cli/agent-client/impl.js +109 -0
- package/dist/modules/cli/agent-client/impl.js.map +1 -0
- package/dist/modules/cli/agent-client/interface.js +11 -0
- package/dist/modules/cli/agent-client/interface.js.map +1 -0
- package/dist/modules/cli/git/impl.js +124 -0
- package/dist/modules/cli/git/impl.js.map +1 -0
- package/dist/modules/cli/git/interface.js +3 -0
- package/dist/modules/cli/git/interface.js.map +1 -0
- package/dist/modules/cli/git/submodule/fix.js +257 -0
- package/dist/modules/cli/git/submodule/fix.js.map +1 -0
- package/dist/modules/cli/git/submodule/git-state.js +154 -0
- package/dist/modules/cli/git/submodule/git-state.js.map +1 -0
- package/dist/modules/cli/git/submodule/gitmodules.js +113 -0
- package/dist/modules/cli/git/submodule/gitmodules.js.map +1 -0
- package/dist/modules/cli/git/submodule/printer.js +232 -0
- package/dist/modules/cli/git/submodule/printer.js.map +1 -0
- package/dist/modules/cli/git/submodule/resolve-git-dir.js +16 -0
- package/dist/modules/cli/git/submodule/resolve-git-dir.js.map +1 -0
- package/dist/modules/cli/github/impl.js +137 -0
- package/dist/modules/cli/github/impl.js.map +1 -0
- package/dist/modules/cli/github/interface.js +2 -0
- package/dist/modules/cli/github/interface.js.map +1 -0
- package/dist/modules/cli/index.js +9 -0
- package/dist/modules/cli/index.js.map +1 -0
- package/dist/modules/common/crypto/impl.js +133 -0
- package/dist/modules/common/crypto/impl.js.map +1 -0
- package/dist/modules/common/crypto/interface.js +2 -0
- package/dist/modules/common/crypto/interface.js.map +1 -0
- package/dist/modules/common/index.js +10 -0
- package/dist/modules/common/index.js.map +1 -0
- package/dist/modules/common/kep-map-store/impl.js +42 -0
- package/dist/modules/common/kep-map-store/impl.js.map +1 -0
- package/dist/modules/common/kep-map-store/interface.js +2 -0
- package/dist/modules/common/kep-map-store/interface.js.map +1 -0
- package/dist/modules/common/os-platform/caller-info.js +132 -0
- package/dist/modules/common/os-platform/caller-info.js.map +1 -0
- package/dist/modules/common/os-platform/impl.js +95 -0
- package/dist/modules/common/os-platform/impl.js.map +1 -0
- package/dist/modules/common/os-platform/interface.js +2 -0
- package/dist/modules/common/os-platform/interface.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/index.js +12 -0
- package/dist/modules/common/os-platform/virtual-hid/index.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/linux.js +5 -0
- package/dist/modules/common/os-platform/virtual-hid/linux.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/mac.js +119 -0
- package/dist/modules/common/os-platform/virtual-hid/mac.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/windows.js +5 -0
- package/dist/modules/common/os-platform/virtual-hid/windows.js.map +1 -0
- package/dist/modules/common/ssh-config/impl.js +116 -0
- package/dist/modules/common/ssh-config/impl.js.map +1 -0
- package/dist/modules/common/ssh-config/interface.js +10 -0
- package/dist/modules/common/ssh-config/interface.js.map +1 -0
- package/dist/modules/ssh-agent/index.js +8 -0
- package/dist/modules/ssh-agent/index.js.map +1 -0
- package/dist/modules/ssh-agent/session/impl.js +265 -0
- package/dist/modules/ssh-agent/session/impl.js.map +1 -0
- package/dist/modules/ssh-agent/session/interface-rpc.js +20 -0
- package/dist/modules/ssh-agent/session/interface-rpc.js.map +1 -0
- package/dist/modules/ssh-agent/session/interface.js +32 -0
- package/dist/modules/ssh-agent/session/interface.js.map +1 -0
- package/dist/modules/ssh-agent/session/passkey-prf-page.js +204 -0
- package/dist/modules/ssh-agent/session/passkey-prf-page.js.map +1 -0
- package/dist/modules/ssh-agent/session/prf-flow.js +103 -0
- package/dist/modules/ssh-agent/session/prf-flow.js.map +1 -0
- package/dist/modules/ssh-agent/ssh-agent/impl.js +117 -0
- package/dist/modules/ssh-agent/ssh-agent/impl.js.map +1 -0
- package/dist/modules/ssh-agent/ssh-agent/interface.js +2 -0
- package/dist/modules/ssh-agent/ssh-agent/interface.js.map +1 -0
- package/dist/scripts/build.js +410 -0
- package/dist/scripts/build.js.map +1 -0
- package/dist/scripts/check-npm-version.js +8 -0
- package/dist/scripts/check-npm-version.js.map +1 -0
- package/dist/scripts/kill-existing-agent.js +21 -0
- package/dist/scripts/kill-existing-agent.js.map +1 -0
- package/dist/scripts/publish.js +46 -0
- package/dist/scripts/publish.js.map +1 -0
- package/package.json +52 -11
- package/src/commands/git/hosts.ts +20 -0
- package/src/commands/git/index.ts +9 -0
- package/src/commands/git/setup.ts +182 -0
- package/src/commands/index.ts +15 -0
- package/src/commands/npm/index.ts +8 -0
- package/src/commands/npm/setup/git-remote.ts +78 -0
- package/src/commands/npm/setup/index.ts +189 -0
- package/src/commands/npm/setup/npm-auth.ts +58 -0
- package/src/commands/npm/setup/package-json-validate.ts +68 -0
- package/src/commands/npm/setup/package-registry.ts +24 -0
- package/src/commands/npm/setup/publish-workflow.yml +35 -0
- package/src/commands/npm/setup/setup.test.ts +117 -0
- package/src/commands/npm/setup/trusted-publishing.ts +106 -0
- package/src/commands/npm/setup/workflow.ts +20 -0
- package/src/commands/ssh/get-sk-credential.ts +110 -0
- package/src/commands/ssh/index.ts +8 -0
- package/src/commands/upgrade.ts +220 -0
- package/src/common/command.ts +16 -0
- package/src/common/constants.ts +26 -0
- package/src/common/effective-modules-extensions.ts +24 -0
- package/src/common/log.ts +3 -0
- package/src/common/render-caller-tree.ts +22 -0
- package/src/entrypoints/ozy-signing-agent.ts +48 -0
- package/src/entrypoints/ozy-ssh-keygen.ts +10 -0
- package/src/entrypoints/ozy-virtual-security-key.ts +21 -0
- package/src/entrypoints/ozy.ts +2 -0
- package/src/index.ts +1 -0
- package/src/internal/thing.ts +3 -0
- package/src/modules/cli/agent-client/impl.ts +152 -0
- package/src/modules/cli/agent-client/interface.ts +17 -0
- package/src/modules/cli/git/impl.ts +133 -0
- package/src/modules/cli/git/interface.ts +23 -0
- package/src/modules/cli/git/submodule/fix.ts +298 -0
- package/src/modules/cli/git/submodule/git-state.ts +217 -0
- package/src/modules/cli/git/submodule/gitmodules.ts +126 -0
- package/src/modules/cli/git/submodule/printer.ts +256 -0
- package/src/modules/cli/git/submodule/resolve-git-dir.ts +14 -0
- package/src/modules/cli/github/impl.ts +169 -0
- package/src/modules/cli/github/interface.ts +29 -0
- package/src/modules/cli/index.ts +16 -0
- package/src/modules/common/crypto/impl.ts +173 -0
- package/src/modules/common/crypto/interface.ts +20 -0
- package/src/modules/common/index.ts +19 -0
- package/src/modules/common/kep-map-store/impl.ts +47 -0
- package/src/modules/common/kep-map-store/interface.ts +9 -0
- package/src/modules/common/os-platform/caller-info.ts +152 -0
- package/src/modules/common/os-platform/impl.ts +91 -0
- package/src/modules/common/os-platform/interface.ts +16 -0
- package/src/modules/common/os-platform/virtual-hid/index.ts +12 -0
- package/src/modules/common/os-platform/virtual-hid/linux.ts +7 -0
- package/src/modules/common/os-platform/virtual-hid/mac.ts +150 -0
- package/src/modules/common/os-platform/virtual-hid/windows.ts +7 -0
- package/src/modules/common/ssh-config/impl.ts +130 -0
- package/src/modules/common/ssh-config/interface.ts +33 -0
- package/src/modules/ssh-agent/index.ts +13 -0
- package/src/modules/ssh-agent/session/agent-notes.md +210 -0
- package/src/modules/ssh-agent/session/design.md +2 -0
- package/src/modules/ssh-agent/session/html-refactor.md +23 -0
- package/src/modules/ssh-agent/session/impl.ts +328 -0
- package/src/modules/ssh-agent/session/interface-rpc.ts +24 -0
- package/src/modules/ssh-agent/session/interface.ts +62 -0
- package/src/modules/ssh-agent/session/passkey-prf-page.ts +224 -0
- package/src/modules/ssh-agent/session/prf-flow.ts +152 -0
- package/src/modules/ssh-agent/ssh-agent/impl.ts +136 -0
- package/src/modules/ssh-agent/ssh-agent/interface.ts +9 -0
- package/src/scripts/build.ts +504 -0
- package/src/scripts/check-npm-version.ts +7 -0
- package/src/scripts/command-launcher.js.ejs +168 -0
- package/src/scripts/kill-existing-agent.ts +22 -0
- package/src/scripts/publish.ts +47 -0
- package/src/scripts/windows-bin-shims.md +1104 -0
- package/multi-call-binary +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CallerProcess } from "@/modules/common/os-platform/interface";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
import type { EffectGen } from "effective-modules";
|
|
4
|
+
import type { SSHPubkey } from "@/modules/common/crypto/impl";
|
|
5
|
+
|
|
6
|
+
export const SessionError = Schema.TaggedUnion({
|
|
7
|
+
SessionTimedOut: {seconds: Schema.Number},
|
|
8
|
+
WebAuthnCancelled: {},
|
|
9
|
+
InternalError: {reason: Schema.String },
|
|
10
|
+
PubkeyMismatch: {expected: Schema.String, derived: Schema.String },
|
|
11
|
+
PubkeyNotRegistered: {pubkey: Schema.String},
|
|
12
|
+
InterruptingSession: {id: Schema.String},
|
|
13
|
+
UnexpectedSignRequest: {},
|
|
14
|
+
});
|
|
15
|
+
export type SessionError = Schema.Schema.Type<typeof SessionError>
|
|
16
|
+
|
|
17
|
+
export const SetupInput = Schema.Struct({
|
|
18
|
+
pubkey: Schema.OptionFromNullOr(Schema.String),
|
|
19
|
+
credentialId: Schema.OptionFromNullOr(Schema.String),
|
|
20
|
+
username: Schema.String,
|
|
21
|
+
});
|
|
22
|
+
export type SetupInput = Schema.Schema.Type<typeof SetupInput>;
|
|
23
|
+
|
|
24
|
+
export const SetupOutput = Schema.Struct({
|
|
25
|
+
pubkey: Schema.String,
|
|
26
|
+
credentialId: Schema.String
|
|
27
|
+
});
|
|
28
|
+
export type SetupOutput = Schema.Schema.Type<typeof SetupOutput>;
|
|
29
|
+
|
|
30
|
+
export interface SignInput {
|
|
31
|
+
data: Buffer;
|
|
32
|
+
pubkey: SSHPubkey;
|
|
33
|
+
callerTree: CallerProcess[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const ExpectedSignRequest = Schema.Struct({
|
|
37
|
+
expectedCallerChain: Schema.Array(Schema.Struct({
|
|
38
|
+
command: Schema.String,
|
|
39
|
+
directory: Schema.OptionFromNullOr(Schema.String)
|
|
40
|
+
}))
|
|
41
|
+
});
|
|
42
|
+
export type ExpectedSignRequest = Schema.Schema.Type<typeof ExpectedSignRequest>;
|
|
43
|
+
|
|
44
|
+
export const StartSessionInput = Schema.Struct({
|
|
45
|
+
expectedSignRequests: Schema.Array(ExpectedSignRequest),
|
|
46
|
+
expectedCommonAncestorPID: Schema.Number,
|
|
47
|
+
timeoutSeconds: Schema.Number,
|
|
48
|
+
pubkey: Schema.String
|
|
49
|
+
});
|
|
50
|
+
export type StartSessionInput = Schema.Schema.Type<typeof StartSessionInput>;
|
|
51
|
+
|
|
52
|
+
export interface ISession {
|
|
53
|
+
startSession(input: StartSessionInput): EffectGen<void, SessionError>;
|
|
54
|
+
sign(input: SignInput): EffectGen<Buffer, SessionError>;
|
|
55
|
+
/*
|
|
56
|
+
Runs PRF directly. Create key from existing credential or create new credential,
|
|
57
|
+
verify against existing pubkey (if pubkey sent).
|
|
58
|
+
Register it in map.
|
|
59
|
+
*/
|
|
60
|
+
setup(input: SetupInput): EffectGen<SetupOutput, SessionError>;
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import { FUTURE_TOOL_NAME } from "@/common/constants";
|
|
3
|
+
import type { ECDHKeyPair } from "@/modules/common/crypto/interface";
|
|
4
|
+
import { CredentialId } from "@/modules/common/crypto/impl";
|
|
5
|
+
import { PrfInput } from "./prf-flow";
|
|
6
|
+
|
|
7
|
+
export interface UseOrCreatePasskeyPageInput {
|
|
8
|
+
agentKeyPair: ECDHKeyPair;
|
|
9
|
+
challenge: string;
|
|
10
|
+
context: PrfInput;
|
|
11
|
+
transportKeyContext: string;
|
|
12
|
+
username: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Mode = 'credential' | 'selectExisting' | 'create' | 'createOrSelect';
|
|
16
|
+
type ChainNode = { command: string; directory: Option.Option<string> };
|
|
17
|
+
type TreeNode = { label: string; children: TreeNode[] };
|
|
18
|
+
|
|
19
|
+
function escHtml(s: string): string {
|
|
20
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function nodeLabel(node: ChainNode): string {
|
|
24
|
+
const dir = Option.getOrElse(() => '(cannot load dir)')(node.directory);
|
|
25
|
+
return `${node.command} ${dir}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function findCommonPrefixLen(chains: ReadonlyArray<ReadonlyArray<ChainNode>>): number {
|
|
29
|
+
if (chains.length <= 1) return chains[0]?.length ?? 0;
|
|
30
|
+
let len = 0;
|
|
31
|
+
for (let i = 0; ; i++) {
|
|
32
|
+
if (chains.some(c => i >= c.length)) break;
|
|
33
|
+
const first = chains[0]![i]!;
|
|
34
|
+
if (chains.some(c => c[i]!.command !== first.command)) break;
|
|
35
|
+
len = i + 1;
|
|
36
|
+
}
|
|
37
|
+
return len;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildTreeNodes(chains: ReadonlyArray<ReadonlyArray<ChainNode>>, from: number): TreeNode[] {
|
|
41
|
+
const groups = new Map<string, { chains: ReadonlyArray<ChainNode>[]; node: ChainNode }>();
|
|
42
|
+
for (const chain of chains) {
|
|
43
|
+
if (from >= chain.length) continue;
|
|
44
|
+
const node = chain[from]!;
|
|
45
|
+
const key = `${node.command}|${Option.getOrElse(() => '')(node.directory)}`;
|
|
46
|
+
const existing = groups.get(key);
|
|
47
|
+
if (existing) {
|
|
48
|
+
existing.chains.push(chain);
|
|
49
|
+
} else {
|
|
50
|
+
groups.set(key, { chains: [chain], node });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return Array.from(groups.values()).map(({ chains: groupChains, node }) => ({
|
|
54
|
+
label: nodeLabel(node),
|
|
55
|
+
children: buildTreeNodes(groupChains, from + 1),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderTreeLines(nodes: TreeNode[], prefix: string): string[] {
|
|
60
|
+
const lines: string[] = [];
|
|
61
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
62
|
+
const node = nodes[i]!;
|
|
63
|
+
const isLast = i === nodes.length - 1;
|
|
64
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
65
|
+
const childPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
66
|
+
lines.push(escHtml(prefix + connector + node.label));
|
|
67
|
+
lines.push(...renderTreeLines(node.children, childPrefix));
|
|
68
|
+
}
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildSignRequestTreeHtml(chains: ReadonlyArray<ReadonlyArray<ChainNode>>): string {
|
|
73
|
+
if (chains.length === 0) return '';
|
|
74
|
+
const commonPrefixLen = findCommonPrefixLen(chains);
|
|
75
|
+
const minLen = Math.min(...chains.map(c => c.length));
|
|
76
|
+
// Show at least 3 levels per branch; always include divergence point
|
|
77
|
+
const displayStart = Math.max(0, Math.min(commonPrefixLen, minLen - 3));
|
|
78
|
+
|
|
79
|
+
const rootNodes = buildTreeNodes(chains, displayStart);
|
|
80
|
+
const lines: string[] = [];
|
|
81
|
+
|
|
82
|
+
if (rootNodes.length === 1) {
|
|
83
|
+
lines.push(escHtml(rootNodes[0]!.label));
|
|
84
|
+
lines.push(...renderTreeLines(rootNodes[0]!.children, ''));
|
|
85
|
+
} else {
|
|
86
|
+
lines.push(...renderTreeLines(rootNodes, ''));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return `<pre class="tree">${lines.join('\n')}</pre>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getContextInfo(context: PrfInput): { contextHtml: string; credentialId: string | null; mode: Mode; friendlyName: string | null } {
|
|
93
|
+
return PrfInput.$match(context, {
|
|
94
|
+
DerivePubkeyOnly: ({ credentialId: credOpt, pubkey }) => {
|
|
95
|
+
const credIdBase58 = Option.getOrNull(credOpt);
|
|
96
|
+
const credIdBase64 = credIdBase58 ? new CredentialId(credIdBase58).base64 : null;
|
|
97
|
+
const mode: Mode = credIdBase64 !== null ? 'credential'
|
|
98
|
+
: Option.isSome(pubkey) ? 'selectExisting'
|
|
99
|
+
: 'createOrSelect';
|
|
100
|
+
const friendlyName = credIdBase58 ? new CredentialId(credIdBase58).humanReadableName : null;
|
|
101
|
+
const contextHtml = friendlyName && credIdBase58
|
|
102
|
+
? `<p style="color:#666;font-size:.95rem">Setup — confirming ownership of <strong>${escHtml(friendlyName)}</strong> (${escHtml(credIdBase58)})</p>`
|
|
103
|
+
: '<p style="color:#666;font-size:.95rem">Setup — create or select a passkey for SSH signing</p>';
|
|
104
|
+
return { contextHtml, credentialId: credIdBase64, mode, friendlyName };
|
|
105
|
+
},
|
|
106
|
+
DerivePubkeyForRequests: ({ session }) => {
|
|
107
|
+
const chains = session.definition.expectedSignRequests.map(r => r.expectedCallerChain);
|
|
108
|
+
const friendlyName = new CredentialId(session.credentialId).humanReadableName;
|
|
109
|
+
const contextHtml = `<p style="color:#666;font-size:.95rem">Signing with <strong>${escHtml(friendlyName)}</strong> (${escHtml(session.credentialId)})</p>${buildSignRequestTreeHtml(chains)}`;
|
|
110
|
+
return {
|
|
111
|
+
contextHtml,
|
|
112
|
+
credentialId: new CredentialId(session.credentialId).base64,
|
|
113
|
+
mode: 'credential' as Mode,
|
|
114
|
+
friendlyName,
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buttonsHtml(mode: Mode, friendlyName: string | null): string {
|
|
121
|
+
if (mode === 'createOrSelect') {
|
|
122
|
+
return `<button class="btn" onclick="btnClick('create')">Create new passkey</button>` +
|
|
123
|
+
`<button class="btn" onclick="btnClick('selectExisting')">Sign with existing passkey</button>`;
|
|
124
|
+
}
|
|
125
|
+
const label = mode === 'credential' && friendlyName ? `Sign with ${friendlyName}`
|
|
126
|
+
: mode === 'selectExisting' ? 'Sign with existing passkey'
|
|
127
|
+
: 'Create new passkey';
|
|
128
|
+
return `<button class="btn" onclick="btnClick()">Authorize — ${label}</button>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function useOrCreatePasskeyPage(input: UseOrCreatePasskeyPageInput): string {
|
|
132
|
+
const { agentKeyPair, challenge, context, transportKeyContext, username } = input;
|
|
133
|
+
const capitalizedName = FUTURE_TOOL_NAME.slice(0, 1).toLocaleUpperCase() + FUTURE_TOOL_NAME.slice(1);
|
|
134
|
+
const title = `${capitalizedName} SSH Auth`;
|
|
135
|
+
|
|
136
|
+
const { contextHtml, credentialId, mode, friendlyName } = getContextInfo(context);
|
|
137
|
+
|
|
138
|
+
const cfg = JSON.stringify({
|
|
139
|
+
agentPublicKey: agentKeyPair.base64Pubkey,
|
|
140
|
+
challenge,
|
|
141
|
+
credentialId,
|
|
142
|
+
mode,
|
|
143
|
+
username,
|
|
144
|
+
}).replace(/</g, "\\u003c");
|
|
145
|
+
|
|
146
|
+
return `<!DOCTYPE html>
|
|
147
|
+
<html>
|
|
148
|
+
<head><title>${title}</title><meta charset="utf-8">
|
|
149
|
+
<style>
|
|
150
|
+
body{font-family:system-ui,sans-serif;max-width:560px;margin:80px auto;padding:0 20px;text-align:center}
|
|
151
|
+
h1{font-size:1.5rem}#st{font-size:1.1rem;margin-top:20px}.ok{color:#060}.err{color:#d00}
|
|
152
|
+
.btn{margin:8px 4px;padding:10px 28px;font-size:1rem;cursor:pointer}.btn:disabled{cursor:default;opacity:.5}
|
|
153
|
+
.tree{font-family:monospace;text-align:left;font-size:.85rem;background:#f5f5f5;padding:12px;border-radius:6px;overflow-x:auto;margin:16px 0}
|
|
154
|
+
</style>
|
|
155
|
+
</head>
|
|
156
|
+
<body>
|
|
157
|
+
<h1>${title}</h1>
|
|
158
|
+
${contextHtml}
|
|
159
|
+
<div id="trust-row" style="margin-bottom:16px;color:#aaa;font-size:.9rem"><label><input type="checkbox" id="trust" onchange="trustChanged()"> trust this device (cache SSH private key) for <input type="number" id="mins" value="15" min="1" disabled style="width:4rem;color:#aaa"> minutes</label></div>
|
|
160
|
+
${buttonsHtml(mode, friendlyName)}
|
|
161
|
+
<p id="st"></p>
|
|
162
|
+
<script>
|
|
163
|
+
const C=${cfg};
|
|
164
|
+
function bu(b){return btoa(String.fromCharCode(...new Uint8Array(b))).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,'')}
|
|
165
|
+
function fu(s){const b=atob(s.replace(/-/g,'+').replace(/_/g,'/'));const u=new Uint8Array(b.length);for(let i=0;i<b.length;i++)u[i]=b.charCodeAt(i);return u}
|
|
166
|
+
function btnClick(m){document.querySelectorAll('.btn').forEach(b=>b.disabled=true);main(m);}
|
|
167
|
+
function trustChanged(){const c=document.getElementById('trust').checked;const m=document.getElementById('mins');document.getElementById('trust-row').style.color=c?'':'#aaa';m.disabled=!c;m.style.color=c?'':'#aaa';}
|
|
168
|
+
async function main(modeOverride){
|
|
169
|
+
const mode=modeOverride??C.mode;
|
|
170
|
+
const st=document.getElementById('st');
|
|
171
|
+
const fid=new URLSearchParams(location.search).get('id');
|
|
172
|
+
try{
|
|
173
|
+
const ch=fu(C.challenge);
|
|
174
|
+
const salt=new Uint8Array(32);new TextEncoder().encodeInto('${FUTURE_TOOL_NAME}-prf-v1',salt);
|
|
175
|
+
let cred;
|
|
176
|
+
if(mode==='credential'){
|
|
177
|
+
cred=await navigator.credentials.get({publicKey:{
|
|
178
|
+
challenge:ch,userVerification:'required',
|
|
179
|
+
allowCredentials:[{type:'public-key',id:fu(C.credentialId)}],
|
|
180
|
+
extensions:{prf:{eval:{first:salt}}}
|
|
181
|
+
}});
|
|
182
|
+
}else if(mode==='selectExisting'){
|
|
183
|
+
cred=await navigator.credentials.get({publicKey:{
|
|
184
|
+
challenge:ch,userVerification:'required',
|
|
185
|
+
extensions:{prf:{eval:{first:salt}}}
|
|
186
|
+
}});
|
|
187
|
+
}else{
|
|
188
|
+
cred=await navigator.credentials.create({publicKey:{
|
|
189
|
+
challenge:ch,rp:{name:'${FUTURE_TOOL_NAME} challenge'},
|
|
190
|
+
user:{id:crypto.getRandomValues(new Uint8Array(16)),name:C.username,displayName:C.username+' — ${capitalizedName} SSH Key'},
|
|
191
|
+
pubKeyCredParams:[{alg:-7,type:'public-key'}],
|
|
192
|
+
authenticatorSelection:{authenticatorAttachment:'platform',userVerification:'required',residentKey:'required'},
|
|
193
|
+
extensions:{prf:{eval:{first:salt}}}
|
|
194
|
+
}});
|
|
195
|
+
}
|
|
196
|
+
const prf=cred.getClientExtensionResults().prf?.results?.first;
|
|
197
|
+
if(!prf)throw Object.assign(new Error('PRF extension not returned — authenticator may not support PRF'),{noRetry:true});
|
|
198
|
+
const ap=await crypto.subtle.importKey('raw',fu(C.agentPublicKey),{name:'ECDH',namedCurve:'P-256'},false,[]);
|
|
199
|
+
const bkp=await crypto.subtle.generateKey({name:'ECDH',namedCurve:'P-256'},true,['deriveBits']);
|
|
200
|
+
const sh=await crypto.subtle.deriveBits({name:'ECDH',public:ap},bkp.privateKey,256);
|
|
201
|
+
const hk=await crypto.subtle.importKey('raw',sh,'HKDF',false,['deriveKey']);
|
|
202
|
+
const ak=await crypto.subtle.deriveKey(
|
|
203
|
+
{name:'HKDF',hash:'SHA-256',salt:new Uint8Array(32),info:new TextEncoder().encode('${transportKeyContext}')},
|
|
204
|
+
hk,{name:'AES-GCM',length:256},false,['encrypt']
|
|
205
|
+
);
|
|
206
|
+
const iv=crypto.getRandomValues(new Uint8Array(12));
|
|
207
|
+
const enc=await crypto.subtle.encrypt({name:'AES-GCM',iv},ak,prf);
|
|
208
|
+
const bp=await crypto.subtle.exportKey('raw',bkp.publicKey);
|
|
209
|
+
const cm=document.getElementById('trust').checked?Math.max(1,parseInt(document.getElementById('mins').value,10)||15):undefined;
|
|
210
|
+
const r=await fetch('/seed?id='+fid,{method:'POST',headers:{'Content-Type':'application/json'},
|
|
211
|
+
body:JSON.stringify({encryptedSeed:bu(enc),iv:bu(iv),browserPublicKey:bu(bp),credentialId:bu(cred.rawId),cacheMinutes:cm})});
|
|
212
|
+
if(!r.ok)throw new Error('Agent returned '+r.status);
|
|
213
|
+
const d=await r.json();
|
|
214
|
+
st.className='ok';st.textContent='Authorized as '+d.friendlyName+'. You can close this tab.';
|
|
215
|
+
}catch(e){
|
|
216
|
+
const cancelled=e&&(e.name==='NotAllowedError'||e.name==='AbortError');
|
|
217
|
+
await fetch('/error?id='+fid,{method:'POST',headers:{'Content-Type':'application/json'},
|
|
218
|
+
body:JSON.stringify({cancelled,message:e?.message??String(e)})}).catch(()=>{});
|
|
219
|
+
st.className='err';st.textContent=(cancelled?'Cancelled or passkey not found.':'Error: '+(e?.message??e))+' You can close this tab.';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
</script>
|
|
223
|
+
</body></html>`;
|
|
224
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Effect, Option, pipe, Deferred, Scope, Data, Result } from "effect";
|
|
2
|
+
import { effunct, type EffectGen } from "effective-modules";
|
|
3
|
+
import { SessionError } from "./interface";
|
|
4
|
+
import { FUTURE_TOOL_NAME } from "@/common/constants";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { CredentialId } from "@/modules/common/crypto/impl";
|
|
7
|
+
import { useOrCreatePasskeyPage } from "./passkey-prf-page";
|
|
8
|
+
import { CommonModules, commonModules } from "@/modules/common";
|
|
9
|
+
import { log } from "@/common/log";
|
|
10
|
+
import { SSHKeyPair } from "@/modules/common/crypto/impl";
|
|
11
|
+
import type { ActiveSession } from "./impl";
|
|
12
|
+
|
|
13
|
+
type BrowserResult = {
|
|
14
|
+
seed: Uint8Array;
|
|
15
|
+
credentialId: CredentialId;
|
|
16
|
+
cacheMinutes?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type PrfResult = {
|
|
20
|
+
keyPair: SSHKeyPair;
|
|
21
|
+
credentialId: CredentialId;
|
|
22
|
+
cacheMinutes?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type PrfInput = Data.TaggedEnum<{
|
|
26
|
+
DerivePubkeyOnly: {
|
|
27
|
+
pubkey: Option.Option<string>;
|
|
28
|
+
credentialId: Option.Option<string>;
|
|
29
|
+
username: string;
|
|
30
|
+
},
|
|
31
|
+
DerivePubkeyForRequests: {
|
|
32
|
+
session: ActiveSession
|
|
33
|
+
}
|
|
34
|
+
}>
|
|
35
|
+
export const PrfInput = Data.taggedEnum<PrfInput>();
|
|
36
|
+
|
|
37
|
+
const TRANSPORT_KEY_CONTEXT = `${FUTURE_TOOL_NAME}-transport-v1`;
|
|
38
|
+
|
|
39
|
+
export function* prfFlow(input: PrfInput): EffectGen<PrfResult, SessionError, CommonModules.Crypto | CommonModules.OSPlatform | Scope.Scope> {
|
|
40
|
+
const crypto = yield* commonModules.Crypto;
|
|
41
|
+
const osPlatform = yield* commonModules.OSPlatform;
|
|
42
|
+
|
|
43
|
+
const flowId = PrfInput.$match(input, {
|
|
44
|
+
DerivePubkeyOnly: () => randomUUID(),
|
|
45
|
+
DerivePubkeyForRequests: ({ session }) => session.id,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const agentKeyPair = yield* pipe(
|
|
49
|
+
effunct(crypto.createECDHKey)(),
|
|
50
|
+
Effect.catch(err => Effect.fail(SessionError.cases.InternalError.make({ reason: err })))
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const challenge = yield* crypto.getRandomChallenge();
|
|
54
|
+
const deferred = yield* Deferred.make<BrowserResult, SessionError>();
|
|
55
|
+
|
|
56
|
+
const server = yield* Effect.acquireRelease(
|
|
57
|
+
Effect.sync(() =>
|
|
58
|
+
Bun.serve({
|
|
59
|
+
port: 0,
|
|
60
|
+
async fetch(req) {
|
|
61
|
+
const url = new URL(req.url);
|
|
62
|
+
if (url.searchParams.get("id") !== flowId) {
|
|
63
|
+
return new Response("Forbidden", { status: 403 });
|
|
64
|
+
}
|
|
65
|
+
if (url.pathname === "/" && req.method === "GET") {
|
|
66
|
+
return new Response(
|
|
67
|
+
useOrCreatePasskeyPage({
|
|
68
|
+
agentKeyPair, challenge, context: input,
|
|
69
|
+
transportKeyContext: TRANSPORT_KEY_CONTEXT,
|
|
70
|
+
username: PrfInput.$match(input, {
|
|
71
|
+
DerivePubkeyOnly: ({ username }) => username,
|
|
72
|
+
DerivePubkeyForRequests: () => '',
|
|
73
|
+
}),
|
|
74
|
+
}),
|
|
75
|
+
{ headers: { "Content-Type": "text/html; charset=utf-8" } }
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (url.pathname === "/seed" && req.method === "POST") {
|
|
79
|
+
try {
|
|
80
|
+
const body = await req.json() as {
|
|
81
|
+
encryptedSeed: string; iv: string;
|
|
82
|
+
browserPublicKey: string; credentialId: string;
|
|
83
|
+
cacheMinutes?: number;
|
|
84
|
+
};
|
|
85
|
+
Effect.runFork(Effect.gen(function* () {
|
|
86
|
+
const innerResult = yield* Effect.result(Effect.gen(function* () {
|
|
87
|
+
const decrypted = yield* crypto.decrypt({
|
|
88
|
+
senderPubkey: body.browserPublicKey,
|
|
89
|
+
receiverKey: agentKeyPair.keyPair,
|
|
90
|
+
cipherText: body.encryptedSeed,
|
|
91
|
+
iv: body.iv,
|
|
92
|
+
hkdfInfo: TRANSPORT_KEY_CONTEXT
|
|
93
|
+
});
|
|
94
|
+
const completed = yield* Deferred.succeed(deferred, {
|
|
95
|
+
seed: decrypted,
|
|
96
|
+
credentialId: CredentialId.fromBase64(body.credentialId),
|
|
97
|
+
cacheMinutes: body.cacheMinutes,
|
|
98
|
+
});
|
|
99
|
+
if (!completed) {
|
|
100
|
+
yield* Effect.fail(SessionError.cases.InternalError.make({
|
|
101
|
+
reason: 'Detected multiple POSTs to seed endpoint'
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
}));
|
|
105
|
+
if (Result.isFailure(innerResult)) {
|
|
106
|
+
console.error('[/seed fork error]', innerResult.failure);
|
|
107
|
+
yield* Deferred.fail(deferred, SessionError.cases.InternalError.make({
|
|
108
|
+
reason: `Seed decryption failed: ${String(innerResult.failure)}`
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
return new Response(JSON.stringify({ ok: true, friendlyName: CredentialId.fromBase64(body.credentialId).humanReadableName }), {
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
});
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error("[/seed handler error]", e);
|
|
117
|
+
Effect.runFork(Deferred.fail(deferred, SessionError.cases.InternalError.make({
|
|
118
|
+
reason: (e as Error).toString()
|
|
119
|
+
})));
|
|
120
|
+
return new Response(JSON.stringify({ error: String(e) }), {
|
|
121
|
+
status: 500, headers: { "Content-Type": "application/json" },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (url.pathname === "/error" && req.method === "POST") {
|
|
126
|
+
const body = await req.json() as { cancelled: boolean; message: string };
|
|
127
|
+
const error = body.cancelled
|
|
128
|
+
? SessionError.cases.WebAuthnCancelled.make({})
|
|
129
|
+
: SessionError.cases.InternalError.make({ reason: body.message });
|
|
130
|
+
Effect.runFork(Deferred.fail(deferred, error));
|
|
131
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
132
|
+
headers: { "Content-Type": "application/json" },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return new Response("Not found", { status: 404 });
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
),
|
|
139
|
+
(server) => Effect.sync(() => server.stop())
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
log(`Opening browser for WebAuthn PRF on port ${server.port}`);
|
|
143
|
+
yield* osPlatform.openBrowserWindow(`http://localhost:${server.port}/?id=${flowId}`);
|
|
144
|
+
|
|
145
|
+
const { seed, credentialId, cacheMinutes } = yield* Deferred.await(deferred);
|
|
146
|
+
|
|
147
|
+
const keyPair = yield* pipe(
|
|
148
|
+
SSHKeyPair.fromSeed(seed, `${FUTURE_TOOL_NAME}-key-v1`),
|
|
149
|
+
Effect.mapError(reason => SessionError.cases.InternalError.make({ reason }))
|
|
150
|
+
);
|
|
151
|
+
return { keyPair, credentialId, cacheMinutes };
|
|
152
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Effect, pipe, Result, Layer } from "effect";
|
|
2
|
+
import { effunct, implementing, type EffectGen } from "effective-modules";
|
|
3
|
+
import type { ISSHAgent } from "./interface";
|
|
4
|
+
import type { CallerProcess } from "@/modules/common/os-platform/interface";
|
|
5
|
+
import { agentModules } from "@/modules/ssh-agent";
|
|
6
|
+
import { commonModules } from "@/modules/common";
|
|
7
|
+
import { SSHPubkey } from "@/modules/common/crypto/impl";
|
|
8
|
+
import { log } from "@/common/log";
|
|
9
|
+
import { renderCallerTree } from "@/common/render-caller-tree";
|
|
10
|
+
import { AGENT_PID_FILE_PATH, AGENT_SOCK_FILE_PATH } from "@/common/constants";
|
|
11
|
+
import { writeFileSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
const SSH2_AGENT_FAILURE = 5;
|
|
14
|
+
const SSH2_AGENT_REQUEST_IDENTITIES = 11;
|
|
15
|
+
const SSH2_AGENT_IDENTITIES_ANSWER = 12;
|
|
16
|
+
const SSH2_AGENT_SIGN_REQUEST = 13;
|
|
17
|
+
const SSH2_AGENT_SIGN_RESPONSE = 14;
|
|
18
|
+
const SSH2_AGENT_EXTENSION = 27;
|
|
19
|
+
|
|
20
|
+
function u32(n: number): Buffer {
|
|
21
|
+
const b = Buffer.allocUnsafe(4);
|
|
22
|
+
b.writeUInt32BE(n, 0);
|
|
23
|
+
return b;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sshString(s: Buffer): Buffer {
|
|
27
|
+
return Buffer.concat([u32(s.length), s]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildSignResponse(keyType: string, signature: Buffer): Buffer {
|
|
31
|
+
const sigBlob = Buffer.concat([sshString(Buffer.from(keyType)), sshString(signature)]);
|
|
32
|
+
const body = Buffer.concat([Buffer.from([SSH2_AGENT_SIGN_RESPONSE]), sshString(sigBlob)]);
|
|
33
|
+
return Buffer.concat([u32(body.length), body]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function failureResponse(): Buffer {
|
|
37
|
+
const r = Buffer.alloc(5);
|
|
38
|
+
r.writeUInt32BE(1, 0);
|
|
39
|
+
r[4] = SSH2_AGENT_FAILURE;
|
|
40
|
+
return r;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { KeyMapStore, OSPlatform } = commonModules;
|
|
44
|
+
const { SSHAgent, Session } = agentModules;
|
|
45
|
+
|
|
46
|
+
export class SSHAgentImpl extends implementing(SSHAgent).uses(Session, KeyMapStore, OSPlatform) implements ISSHAgent {
|
|
47
|
+
|
|
48
|
+
private log(...items: any[]) {
|
|
49
|
+
log(`[ssh-agent]`, ...items);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
*start(): EffectGen<void, string> {
|
|
53
|
+
const { OSPlatform: { startSocketServer } } = this.dependencies;
|
|
54
|
+
const { handleExtension, handleRequestIdentities, handleSignRequest, log } = this;
|
|
55
|
+
yield* startSocketServer(AGENT_SOCK_FILE_PATH, function*(data, callerChain): EffectGen<Buffer> {
|
|
56
|
+
const msgType = data[4];
|
|
57
|
+
switch (msgType) {
|
|
58
|
+
case SSH2_AGENT_EXTENSION:
|
|
59
|
+
log(`received extensions request`);
|
|
60
|
+
return yield* handleExtension(data);
|
|
61
|
+
case SSH2_AGENT_REQUEST_IDENTITIES:
|
|
62
|
+
log(`received request identities request`);
|
|
63
|
+
return yield* handleRequestIdentities();
|
|
64
|
+
case SSH2_AGENT_SIGN_REQUEST:
|
|
65
|
+
log(`received signature request`);
|
|
66
|
+
return yield* handleSignRequest(data, callerChain);
|
|
67
|
+
default:
|
|
68
|
+
log(`received unknown message type ${msgType}`);
|
|
69
|
+
return failureResponse();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
writeFileSync(AGENT_PID_FILE_PATH, process.pid.toString());
|
|
73
|
+
log(`listening on ${AGENT_SOCK_FILE_PATH}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
*handleExtension(data: Buffer): EffectGen<Buffer, never> {
|
|
77
|
+
const nameLen = data.readUInt32BE(5);
|
|
78
|
+
const name = data.subarray(9, 9 + nameLen).toString("utf8");
|
|
79
|
+
this.log(`extension request for ${name}`);
|
|
80
|
+
// We don't support any extensions, so we fail this outright.
|
|
81
|
+
return yield* Effect.sync(() => failureResponse());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
*handleRequestIdentities(): EffectGen<Buffer, never> {
|
|
85
|
+
const keyMapStore = this.dependencies.KeyMapStore;
|
|
86
|
+
const listResult = yield* pipe(
|
|
87
|
+
effunct(keyMapStore.listPubkeys)(),
|
|
88
|
+
Effect.result
|
|
89
|
+
);
|
|
90
|
+
const pubkeys = Result.isSuccess(listResult) ? listResult.success : [];
|
|
91
|
+
if (Result.isFailure(listResult)) {
|
|
92
|
+
this.log(`Failure to retrieve keys ${listResult.failure}`);
|
|
93
|
+
}
|
|
94
|
+
this.log(`Total keys ${pubkeys.length}`);
|
|
95
|
+
if (pubkeys.length === 0) {
|
|
96
|
+
const body = Buffer.concat([Buffer.from([SSH2_AGENT_IDENTITIES_ANSWER]), u32(0)]);
|
|
97
|
+
return Buffer.concat([u32(body.length), body]);
|
|
98
|
+
}
|
|
99
|
+
const keyEntries = pubkeys.map(pubkey => {
|
|
100
|
+
return Buffer.concat([sshString(pubkey.wire), sshString(Buffer.alloc(0))]);
|
|
101
|
+
});
|
|
102
|
+
const body = Buffer.concat([
|
|
103
|
+
Buffer.from([SSH2_AGENT_IDENTITIES_ANSWER]),
|
|
104
|
+
u32(pubkeys.length),
|
|
105
|
+
...keyEntries,
|
|
106
|
+
]);
|
|
107
|
+
return Buffer.concat([u32(body.length), body]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
*handleSignRequest(data: Buffer, callerChain: CallerProcess[]): EffectGen<Buffer, never> {
|
|
111
|
+
const session = this.dependencies.Session;
|
|
112
|
+
let offset = 5; // skip length(4) + type(1)
|
|
113
|
+
const keyBlobLen = data.readUInt32BE(offset); offset += 4;
|
|
114
|
+
const pubkeyWire = Buffer.from(data.subarray(offset, offset + keyBlobLen)); offset += keyBlobLen;
|
|
115
|
+
const dataToSignLen = data.readUInt32BE(offset); offset += 4;
|
|
116
|
+
const dataToSign = Buffer.from(data.subarray(offset, offset + dataToSignLen));
|
|
117
|
+
|
|
118
|
+
const pubkey = SSHPubkey.fromWire(pubkeyWire);
|
|
119
|
+
|
|
120
|
+
this.log(`Sign request for pubkey ${pubkey.authorizedKey}\n${renderCallerTree(callerChain.slice(-3))}`);
|
|
121
|
+
const signResult = yield* pipe(
|
|
122
|
+
effunct(session.sign)({
|
|
123
|
+
data: dataToSign,
|
|
124
|
+
pubkey,
|
|
125
|
+
callerTree: callerChain
|
|
126
|
+
}),
|
|
127
|
+
Effect.result
|
|
128
|
+
);
|
|
129
|
+
return Result.isSuccess(signResult) ? buildSignResponse(pubkey.keyType, signResult.success) : failureResponse();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public static ServiceLayer = Layer.effectDiscard(Effect.gen(function*() {
|
|
133
|
+
const sshAgent = yield* agentModules.SSHAgent;
|
|
134
|
+
yield* sshAgent.start();
|
|
135
|
+
})).pipe(Layer.provideMerge(this.Layer))
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EffectGen } from "effective-modules";
|
|
2
|
+
import type { CallerProcess } from "@/modules/common/os-platform/interface";
|
|
3
|
+
|
|
4
|
+
export interface ISSHAgent {
|
|
5
|
+
start(): EffectGen<void, string>;
|
|
6
|
+
handleExtension(data: Buffer): EffectGen<Buffer, never>;
|
|
7
|
+
handleRequestIdentities(): EffectGen<Buffer, never>;
|
|
8
|
+
handleSignRequest(data: Buffer, callerChain: CallerProcess[]): EffectGen<Buffer, never>;
|
|
9
|
+
}
|