passwd-sso-cli 0.4.3
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/dist/commands/agent-decrypt.d.ts +11 -0
- package/dist/commands/agent-decrypt.js +317 -0
- package/dist/commands/agent.d.ts +13 -0
- package/dist/commands/agent.js +116 -0
- package/dist/commands/api-key.d.ts +22 -0
- package/dist/commands/api-key.js +118 -0
- package/dist/commands/decrypt.d.ts +17 -0
- package/dist/commands/decrypt.js +108 -0
- package/dist/commands/env.d.ts +15 -0
- package/dist/commands/env.js +102 -0
- package/dist/commands/export.d.ts +7 -0
- package/dist/commands/export.js +99 -0
- package/dist/commands/generate.d.ts +11 -0
- package/dist/commands/generate.js +45 -0
- package/dist/commands/get.d.ts +8 -0
- package/dist/commands/get.js +73 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +66 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +45 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +97 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +62 -0
- package/dist/commands/totp.d.ts +7 -0
- package/dist/commands/totp.js +57 -0
- package/dist/commands/unlock.d.ts +19 -0
- package/dist/commands/unlock.js +125 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +298 -0
- package/dist/lib/api-client.d.ts +22 -0
- package/dist/lib/api-client.js +145 -0
- package/dist/lib/blocked-keys.d.ts +2 -0
- package/dist/lib/blocked-keys.js +23 -0
- package/dist/lib/clipboard.d.ts +8 -0
- package/dist/lib/clipboard.js +79 -0
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/config.js +110 -0
- package/dist/lib/crypto-aad.d.ts +5 -0
- package/dist/lib/crypto-aad.js +44 -0
- package/dist/lib/crypto.d.ts +23 -0
- package/dist/lib/crypto.js +148 -0
- package/dist/lib/migrate.d.ts +8 -0
- package/dist/lib/migrate.js +87 -0
- package/dist/lib/openssh-key-parser.d.ts +17 -0
- package/dist/lib/openssh-key-parser.js +273 -0
- package/dist/lib/output.d.ts +10 -0
- package/dist/lib/output.js +36 -0
- package/dist/lib/paths.d.ts +17 -0
- package/dist/lib/paths.js +39 -0
- package/dist/lib/secrets-config.d.ts +31 -0
- package/dist/lib/secrets-config.js +48 -0
- package/dist/lib/ssh-agent-protocol.d.ts +56 -0
- package/dist/lib/ssh-agent-protocol.js +108 -0
- package/dist/lib/ssh-agent-socket.d.ts +20 -0
- package/dist/lib/ssh-agent-socket.js +187 -0
- package/dist/lib/ssh-key-agent.d.ts +54 -0
- package/dist/lib/ssh-key-agent.js +197 -0
- package/dist/lib/totp.d.ts +10 -0
- package/dist/lib/totp.js +31 -0
- package/dist/lib/vault-state.d.ts +15 -0
- package/dist/lib/vault-state.js +37 -0
- package/package.json +56 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenSSH private key format parser.
|
|
3
|
+
*
|
|
4
|
+
* Handles `-----BEGIN OPENSSH PRIVATE KEY-----` format that
|
|
5
|
+
* Node.js/OpenSSL `createPrivateKey()` cannot always parse
|
|
6
|
+
* (notably encrypted keys using bcrypt-pbkdf).
|
|
7
|
+
*
|
|
8
|
+
* Supports: Ed25519, RSA, ECDSA (encrypted and unencrypted).
|
|
9
|
+
*
|
|
10
|
+
* Reference: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
|
|
11
|
+
*/
|
|
12
|
+
import { createPrivateKey, createDecipheriv } from "node:crypto";
|
|
13
|
+
const OPENSSH_MAGIC = "openssh-key-v1\0";
|
|
14
|
+
/**
|
|
15
|
+
* Parse an OpenSSH private key PEM into a Node.js KeyObject.
|
|
16
|
+
* Falls back to this when `createPrivateKey()` can't handle the format.
|
|
17
|
+
*/
|
|
18
|
+
export async function parseOpenSshPrivateKey(pem, passphrase) {
|
|
19
|
+
const lines = pem.trim().split(/\r?\n/);
|
|
20
|
+
if (lines[0] !== "-----BEGIN OPENSSH PRIVATE KEY-----" ||
|
|
21
|
+
lines[lines.length - 1] !== "-----END OPENSSH PRIVATE KEY-----") {
|
|
22
|
+
throw new Error("Not an OpenSSH private key");
|
|
23
|
+
}
|
|
24
|
+
const b64 = lines.slice(1, -1).join("");
|
|
25
|
+
const buf = Buffer.from(b64, "base64");
|
|
26
|
+
let offset = 0;
|
|
27
|
+
// Verify magic
|
|
28
|
+
const magic = buf.subarray(0, OPENSSH_MAGIC.length).toString("ascii");
|
|
29
|
+
if (magic !== OPENSSH_MAGIC) {
|
|
30
|
+
throw new Error("Invalid OpenSSH key magic");
|
|
31
|
+
}
|
|
32
|
+
offset += OPENSSH_MAGIC.length;
|
|
33
|
+
// Read cipher name
|
|
34
|
+
const cipherName = readString(buf, offset);
|
|
35
|
+
offset += 4 + cipherName.length;
|
|
36
|
+
// Read KDF name
|
|
37
|
+
const kdfName = readString(buf, offset);
|
|
38
|
+
offset += 4 + kdfName.length;
|
|
39
|
+
// Read KDF options
|
|
40
|
+
const kdfOptionsLen = buf.readUInt32BE(offset);
|
|
41
|
+
offset += 4;
|
|
42
|
+
const kdfOptions = buf.subarray(offset, offset + kdfOptionsLen);
|
|
43
|
+
offset += kdfOptionsLen;
|
|
44
|
+
// Number of keys
|
|
45
|
+
const numKeys = buf.readUInt32BE(offset);
|
|
46
|
+
offset += 4;
|
|
47
|
+
if (numKeys !== 1) {
|
|
48
|
+
throw new Error(`Expected 1 key, got ${numKeys}`);
|
|
49
|
+
}
|
|
50
|
+
// Skip public key blob
|
|
51
|
+
const pubKeyLen = buf.readUInt32BE(offset);
|
|
52
|
+
offset += 4 + pubKeyLen;
|
|
53
|
+
// Read private section
|
|
54
|
+
const privSectionLen = buf.readUInt32BE(offset);
|
|
55
|
+
offset += 4;
|
|
56
|
+
let privSection = buf.subarray(offset, offset + privSectionLen);
|
|
57
|
+
// Decrypt if encrypted
|
|
58
|
+
const isEncrypted = cipherName !== "none" || kdfName !== "none";
|
|
59
|
+
if (isEncrypted) {
|
|
60
|
+
if (!passphrase) {
|
|
61
|
+
throw new Error("This SSH key is encrypted but no passphrase is stored. " +
|
|
62
|
+
"Edit the vault entry and add the key passphrase.");
|
|
63
|
+
}
|
|
64
|
+
privSection = await decryptPrivateSection(privSection, cipherName, kdfName, kdfOptions, passphrase);
|
|
65
|
+
}
|
|
66
|
+
// Parse unencrypted private section
|
|
67
|
+
let pOff = 0;
|
|
68
|
+
// Check integers (random, must match)
|
|
69
|
+
const check1 = privSection.readUInt32BE(pOff);
|
|
70
|
+
pOff += 4;
|
|
71
|
+
const check2 = privSection.readUInt32BE(pOff);
|
|
72
|
+
pOff += 4;
|
|
73
|
+
if (check1 !== check2) {
|
|
74
|
+
throw new Error("Check integers mismatch — key data may be corrupted");
|
|
75
|
+
}
|
|
76
|
+
// Key type
|
|
77
|
+
const keyType = readString(privSection, pOff);
|
|
78
|
+
pOff += 4 + keyType.length;
|
|
79
|
+
switch (keyType) {
|
|
80
|
+
case "ssh-ed25519":
|
|
81
|
+
return parseEd25519(privSection, pOff);
|
|
82
|
+
case "ssh-rsa":
|
|
83
|
+
return parseRsa(privSection, pOff);
|
|
84
|
+
case "ecdsa-sha2-nistp256":
|
|
85
|
+
case "ecdsa-sha2-nistp384":
|
|
86
|
+
case "ecdsa-sha2-nistp521":
|
|
87
|
+
return parseEcdsa(privSection, pOff, keyType);
|
|
88
|
+
default:
|
|
89
|
+
throw new Error(`Unsupported OpenSSH key type: ${keyType}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ─── Decryption ───────────────────────────────────────────
|
|
93
|
+
/** Cipher info: Node.js algorithm name, key length, IV length */
|
|
94
|
+
const CIPHER_INFO = {
|
|
95
|
+
"aes256-ctr": { algo: "aes-256-ctr", keyLen: 32, ivLen: 16 },
|
|
96
|
+
"aes256-cbc": { algo: "aes-256-cbc", keyLen: 32, ivLen: 16 },
|
|
97
|
+
"aes128-ctr": { algo: "aes-128-ctr", keyLen: 16, ivLen: 16 },
|
|
98
|
+
"aes128-cbc": { algo: "aes-128-cbc", keyLen: 16, ivLen: 16 },
|
|
99
|
+
"aes192-ctr": { algo: "aes-192-ctr", keyLen: 24, ivLen: 16 },
|
|
100
|
+
"aes192-cbc": { algo: "aes-192-cbc", keyLen: 24, ivLen: 16 },
|
|
101
|
+
};
|
|
102
|
+
async function decryptPrivateSection(encrypted, cipherName, kdfName, kdfOptions, passphrase) {
|
|
103
|
+
if (kdfName !== "bcrypt") {
|
|
104
|
+
throw new Error(`Unsupported KDF: ${kdfName}`);
|
|
105
|
+
}
|
|
106
|
+
const cipher = CIPHER_INFO[cipherName];
|
|
107
|
+
if (!cipher) {
|
|
108
|
+
throw new Error(`Unsupported cipher: ${cipherName}`);
|
|
109
|
+
}
|
|
110
|
+
// Parse KDF options: uint32 salt_len, salt, uint32 rounds
|
|
111
|
+
let kOff = 0;
|
|
112
|
+
const saltLen = kdfOptions.readUInt32BE(kOff);
|
|
113
|
+
kOff += 4;
|
|
114
|
+
const salt = kdfOptions.subarray(kOff, kOff + saltLen);
|
|
115
|
+
kOff += saltLen;
|
|
116
|
+
const rounds = kdfOptions.readUInt32BE(kOff);
|
|
117
|
+
// Derive key + IV using bcrypt-pbkdf
|
|
118
|
+
const { pbkdf } = await import("bcrypt-pbkdf");
|
|
119
|
+
const derivedLen = cipher.keyLen + cipher.ivLen;
|
|
120
|
+
const derived = Buffer.alloc(derivedLen);
|
|
121
|
+
const ret = pbkdf(Buffer.from(passphrase, "utf-8"), passphrase.length, salt, salt.length, derived, derivedLen, rounds);
|
|
122
|
+
if (ret !== 0) {
|
|
123
|
+
throw new Error("bcrypt-pbkdf failed");
|
|
124
|
+
}
|
|
125
|
+
const key = derived.subarray(0, cipher.keyLen);
|
|
126
|
+
const iv = derived.subarray(cipher.keyLen);
|
|
127
|
+
// Decrypt
|
|
128
|
+
const decipher = createDecipheriv(cipher.algo, key, iv);
|
|
129
|
+
decipher.setAutoPadding(false);
|
|
130
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
131
|
+
// Zero derived key material
|
|
132
|
+
derived.fill(0);
|
|
133
|
+
key.fill(0);
|
|
134
|
+
iv.fill(0);
|
|
135
|
+
return decrypted;
|
|
136
|
+
}
|
|
137
|
+
// ─── Ed25519 ──────────────────────────────────────────────
|
|
138
|
+
function parseEd25519(buf, offset) {
|
|
139
|
+
// Public key (32 bytes)
|
|
140
|
+
const pubLen = buf.readUInt32BE(offset);
|
|
141
|
+
offset += 4;
|
|
142
|
+
const pubKey = buf.subarray(offset, offset + pubLen);
|
|
143
|
+
offset += pubLen;
|
|
144
|
+
// Private key (64 bytes = 32-byte seed + 32-byte pubkey)
|
|
145
|
+
// Skip 4-byte private key length prefix
|
|
146
|
+
offset += 4;
|
|
147
|
+
const seed = buf.subarray(offset, offset + 32);
|
|
148
|
+
// Build JWK from raw key material
|
|
149
|
+
return createPrivateKey({
|
|
150
|
+
key: {
|
|
151
|
+
kty: "OKP",
|
|
152
|
+
crv: "Ed25519",
|
|
153
|
+
d: base64url(seed),
|
|
154
|
+
x: base64url(pubKey),
|
|
155
|
+
},
|
|
156
|
+
format: "jwk",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// ─── RSA ──────────────────────────────────────────────────
|
|
160
|
+
function parseRsa(buf, offset) {
|
|
161
|
+
// OpenSSH RSA format: n, e, d, iqmp, p, q
|
|
162
|
+
const n = readMpint(buf, offset);
|
|
163
|
+
offset += 4 + n.length;
|
|
164
|
+
const e = readMpint(buf, offset);
|
|
165
|
+
offset += 4 + e.length;
|
|
166
|
+
const d = readMpint(buf, offset);
|
|
167
|
+
offset += 4 + d.length;
|
|
168
|
+
const iqmp = readMpint(buf, offset);
|
|
169
|
+
offset += 4 + iqmp.length;
|
|
170
|
+
const p = readMpint(buf, offset);
|
|
171
|
+
offset += 4 + p.length;
|
|
172
|
+
const q = readMpint(buf, offset);
|
|
173
|
+
return createPrivateKey({
|
|
174
|
+
key: {
|
|
175
|
+
kty: "RSA",
|
|
176
|
+
n: base64url(stripLeadingZero(n)),
|
|
177
|
+
e: base64url(stripLeadingZero(e)),
|
|
178
|
+
d: base64url(stripLeadingZero(d)),
|
|
179
|
+
p: base64url(stripLeadingZero(p)),
|
|
180
|
+
q: base64url(stripLeadingZero(q)),
|
|
181
|
+
qi: base64url(stripLeadingZero(iqmp)),
|
|
182
|
+
// dp and dq are required by JWK but can be derived
|
|
183
|
+
dp: base64url(stripLeadingZero(modBuf(d, pMinus1(p)))),
|
|
184
|
+
dq: base64url(stripLeadingZero(modBuf(d, pMinus1(q)))),
|
|
185
|
+
},
|
|
186
|
+
format: "jwk",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// ─── ECDSA ────────────────────────────────────────────────
|
|
190
|
+
function parseEcdsa(buf, offset, keyType) {
|
|
191
|
+
// Curve identifier string
|
|
192
|
+
const curveId = readString(buf, offset);
|
|
193
|
+
offset += 4 + curveId.length;
|
|
194
|
+
// Public key point (uncompressed: 0x04 + x + y)
|
|
195
|
+
const pubPointLen = buf.readUInt32BE(offset);
|
|
196
|
+
offset += 4;
|
|
197
|
+
const pubPoint = buf.subarray(offset, offset + pubPointLen);
|
|
198
|
+
offset += pubPointLen;
|
|
199
|
+
// Private key scalar
|
|
200
|
+
const privScalar = readMpint(buf, offset);
|
|
201
|
+
const curveMap = {
|
|
202
|
+
"ecdsa-sha2-nistp256": { crv: "P-256", size: 32 },
|
|
203
|
+
"ecdsa-sha2-nistp384": { crv: "P-384", size: 48 },
|
|
204
|
+
"ecdsa-sha2-nistp521": { crv: "P-521", size: 66 },
|
|
205
|
+
};
|
|
206
|
+
const curve = curveMap[keyType];
|
|
207
|
+
if (!curve)
|
|
208
|
+
throw new Error(`Unsupported ECDSA curve: ${keyType}`);
|
|
209
|
+
// pubPoint format: 0x04 + x (size bytes) + y (size bytes)
|
|
210
|
+
const x = pubPoint.subarray(1, 1 + curve.size);
|
|
211
|
+
const y = pubPoint.subarray(1 + curve.size, 1 + 2 * curve.size);
|
|
212
|
+
const d = stripLeadingZero(privScalar);
|
|
213
|
+
// Pad d to curve size if needed
|
|
214
|
+
const dPadded = d.length < curve.size
|
|
215
|
+
? Buffer.concat([Buffer.alloc(curve.size - d.length), d])
|
|
216
|
+
: d;
|
|
217
|
+
return createPrivateKey({
|
|
218
|
+
key: {
|
|
219
|
+
kty: "EC",
|
|
220
|
+
crv: curve.crv,
|
|
221
|
+
x: base64url(x),
|
|
222
|
+
y: base64url(y),
|
|
223
|
+
d: base64url(dPadded),
|
|
224
|
+
},
|
|
225
|
+
format: "jwk",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// ─── Binary helpers ───────────────────────────────────────
|
|
229
|
+
function readString(buf, offset) {
|
|
230
|
+
const len = buf.readUInt32BE(offset);
|
|
231
|
+
return buf.subarray(offset + 4, offset + 4 + len).toString("utf-8");
|
|
232
|
+
}
|
|
233
|
+
function readMpint(buf, offset) {
|
|
234
|
+
const len = buf.readUInt32BE(offset);
|
|
235
|
+
return buf.subarray(offset + 4, offset + 4 + len);
|
|
236
|
+
}
|
|
237
|
+
function base64url(buf) {
|
|
238
|
+
return Buffer.from(buf)
|
|
239
|
+
.toString("base64")
|
|
240
|
+
.replace(/\+/g, "-")
|
|
241
|
+
.replace(/\//g, "_")
|
|
242
|
+
.replace(/=+$/, "");
|
|
243
|
+
}
|
|
244
|
+
function stripLeadingZero(buf) {
|
|
245
|
+
let i = 0;
|
|
246
|
+
while (i < buf.length - 1 && buf[i] === 0)
|
|
247
|
+
i++;
|
|
248
|
+
return i > 0 ? buf.subarray(i) : buf;
|
|
249
|
+
}
|
|
250
|
+
// ─── BigInt math for RSA dp/dq ────────────────────────────
|
|
251
|
+
function bufToBigInt(buf) {
|
|
252
|
+
let result = 0n;
|
|
253
|
+
for (const byte of buf) {
|
|
254
|
+
result = (result << 8n) | BigInt(byte);
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
function bigIntToBuf(n) {
|
|
259
|
+
if (n === 0n)
|
|
260
|
+
return Buffer.from([0]);
|
|
261
|
+
const hex = n.toString(16);
|
|
262
|
+
const padded = hex.length % 2 ? "0" + hex : hex;
|
|
263
|
+
return Buffer.from(padded, "hex");
|
|
264
|
+
}
|
|
265
|
+
function pMinus1(p) {
|
|
266
|
+
return bufToBigInt(stripLeadingZero(p)) - 1n;
|
|
267
|
+
}
|
|
268
|
+
function modBuf(d, m) {
|
|
269
|
+
const dBig = bufToBigInt(stripLeadingZero(d));
|
|
270
|
+
const result = dBig % m;
|
|
271
|
+
return bigIntToBuf(result);
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=openssh-key-parser.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting for CLI display.
|
|
3
|
+
*/
|
|
4
|
+
export declare function success(message: string): void;
|
|
5
|
+
export declare function error(message: string): void;
|
|
6
|
+
export declare function warn(message: string): void;
|
|
7
|
+
export declare function info(message: string): void;
|
|
8
|
+
export declare function table(headers: string[], rows: string[][]): void;
|
|
9
|
+
export declare function json(data: unknown): void;
|
|
10
|
+
export declare function masked(value: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting for CLI display.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import Table from "cli-table3";
|
|
6
|
+
export function success(message) {
|
|
7
|
+
console.log(chalk.green(`✓ ${message}`));
|
|
8
|
+
}
|
|
9
|
+
export function error(message) {
|
|
10
|
+
console.error(chalk.red(`✗ ${message}`));
|
|
11
|
+
}
|
|
12
|
+
export function warn(message) {
|
|
13
|
+
console.log(chalk.yellow(`! ${message}`));
|
|
14
|
+
}
|
|
15
|
+
export function info(message) {
|
|
16
|
+
console.log(chalk.blue(`ℹ ${message}`));
|
|
17
|
+
}
|
|
18
|
+
export function table(headers, rows) {
|
|
19
|
+
const t = new Table({
|
|
20
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
21
|
+
style: { head: [], border: [] },
|
|
22
|
+
});
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
t.push(row);
|
|
25
|
+
}
|
|
26
|
+
console.log(t.toString());
|
|
27
|
+
}
|
|
28
|
+
export function json(data) {
|
|
29
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30
|
+
}
|
|
31
|
+
export function masked(value) {
|
|
32
|
+
if (value.length <= 4)
|
|
33
|
+
return "****";
|
|
34
|
+
return "*".repeat(value.length - 4) + value.slice(-4);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XDG Base Directory compliant path resolution.
|
|
3
|
+
*
|
|
4
|
+
* Config: $XDG_CONFIG_HOME/passwd-sso/ (default: ~/.config/passwd-sso/)
|
|
5
|
+
* Data: $XDG_DATA_HOME/passwd-sso/ (default: ~/.local/share/passwd-sso/)
|
|
6
|
+
* Legacy: ~/.passwd-sso/ (auto-migrated on first access)
|
|
7
|
+
*/
|
|
8
|
+
/** Resolve config directory (lazy — reads env at call time). */
|
|
9
|
+
export declare function getConfigDir(): string;
|
|
10
|
+
/** Resolve data directory (lazy — reads env at call time). */
|
|
11
|
+
export declare function getDataDir(): string;
|
|
12
|
+
/** Legacy directory path (for migration detection). */
|
|
13
|
+
export declare function getLegacyDir(): string;
|
|
14
|
+
/** Full path to config.json. */
|
|
15
|
+
export declare function getConfigFilePath(): string;
|
|
16
|
+
/** Full path to credentials file. */
|
|
17
|
+
export declare function getCredentialsFilePath(): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XDG Base Directory compliant path resolution.
|
|
3
|
+
*
|
|
4
|
+
* Config: $XDG_CONFIG_HOME/passwd-sso/ (default: ~/.config/passwd-sso/)
|
|
5
|
+
* Data: $XDG_DATA_HOME/passwd-sso/ (default: ~/.local/share/passwd-sso/)
|
|
6
|
+
* Legacy: ~/.passwd-sso/ (auto-migrated on first access)
|
|
7
|
+
*/
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { isAbsolute, join } from "node:path";
|
|
10
|
+
const APP_NAME = "passwd-sso";
|
|
11
|
+
/** Resolve config directory (lazy — reads env at call time). */
|
|
12
|
+
export function getConfigDir() {
|
|
13
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
14
|
+
const base = xdgConfig && isAbsolute(xdgConfig)
|
|
15
|
+
? xdgConfig
|
|
16
|
+
: join(homedir(), ".config");
|
|
17
|
+
return join(base, APP_NAME);
|
|
18
|
+
}
|
|
19
|
+
/** Resolve data directory (lazy — reads env at call time). */
|
|
20
|
+
export function getDataDir() {
|
|
21
|
+
const xdgData = process.env.XDG_DATA_HOME;
|
|
22
|
+
const base = xdgData && isAbsolute(xdgData)
|
|
23
|
+
? xdgData
|
|
24
|
+
: join(homedir(), ".local", "share");
|
|
25
|
+
return join(base, APP_NAME);
|
|
26
|
+
}
|
|
27
|
+
/** Legacy directory path (for migration detection). */
|
|
28
|
+
export function getLegacyDir() {
|
|
29
|
+
return join(homedir(), `.${APP_NAME}`);
|
|
30
|
+
}
|
|
31
|
+
/** Full path to config.json. */
|
|
32
|
+
export function getConfigFilePath() {
|
|
33
|
+
return join(getConfigDir(), "config.json");
|
|
34
|
+
}
|
|
35
|
+
/** Full path to credentials file. */
|
|
36
|
+
export function getCredentialsFilePath() {
|
|
37
|
+
return join(getDataDir(), "credentials");
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .passwd-sso-env.json loader.
|
|
3
|
+
*
|
|
4
|
+
* Schema:
|
|
5
|
+
* {
|
|
6
|
+
* "server": "https://...",
|
|
7
|
+
* "apiKey?": "api_...", // optional — uses /api/v1/ path
|
|
8
|
+
* "secrets": {
|
|
9
|
+
* "ENV_VAR_NAME": { "entry": "<entryId>", "field": "password" }
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Auth flow:
|
|
14
|
+
* apiKey present → /api/v1/passwords (Bearer api_key)
|
|
15
|
+
* apiKey absent → /api/passwords (Bearer extension token via login)
|
|
16
|
+
*/
|
|
17
|
+
export interface SecretMapping {
|
|
18
|
+
entry: string;
|
|
19
|
+
field: string;
|
|
20
|
+
}
|
|
21
|
+
export interface SecretsConfig {
|
|
22
|
+
server: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
secrets: Record<string, SecretMapping>;
|
|
25
|
+
}
|
|
26
|
+
export declare function loadSecretsConfig(configPath?: string): SecretsConfig;
|
|
27
|
+
/**
|
|
28
|
+
* Returns the API path for fetching a single password entry.
|
|
29
|
+
* If apiKey is configured, use the public /api/v1/ path.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getPasswordPath(entryId: string, useV1: boolean): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .passwd-sso-env.json loader.
|
|
3
|
+
*
|
|
4
|
+
* Schema:
|
|
5
|
+
* {
|
|
6
|
+
* "server": "https://...",
|
|
7
|
+
* "apiKey?": "api_...", // optional — uses /api/v1/ path
|
|
8
|
+
* "secrets": {
|
|
9
|
+
* "ENV_VAR_NAME": { "entry": "<entryId>", "field": "password" }
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Auth flow:
|
|
14
|
+
* apiKey present → /api/v1/passwords (Bearer api_key)
|
|
15
|
+
* apiKey absent → /api/passwords (Bearer extension token via login)
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
18
|
+
import { resolve } from "node:path";
|
|
19
|
+
export function loadSecretsConfig(configPath) {
|
|
20
|
+
const filePath = configPath
|
|
21
|
+
? resolve(configPath)
|
|
22
|
+
: resolve(process.cwd(), ".passwd-sso-env.json");
|
|
23
|
+
if (!existsSync(filePath)) {
|
|
24
|
+
throw new Error(`Config file not found: ${filePath}`);
|
|
25
|
+
}
|
|
26
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
if (!parsed.server || typeof parsed.server !== "string") {
|
|
29
|
+
throw new Error("Config file must have a 'server' field.");
|
|
30
|
+
}
|
|
31
|
+
if (!parsed.secrets || typeof parsed.secrets !== "object") {
|
|
32
|
+
throw new Error("Config file must have a 'secrets' field.");
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the API path for fetching a single password entry.
|
|
38
|
+
* If apiKey is configured, use the public /api/v1/ path.
|
|
39
|
+
*/
|
|
40
|
+
export function getPasswordPath(entryId, useV1) {
|
|
41
|
+
if (/[\/\\]/.test(entryId)) {
|
|
42
|
+
throw new Error(`Invalid entry ID: "${entryId}"`);
|
|
43
|
+
}
|
|
44
|
+
return useV1
|
|
45
|
+
? `/api/v1/passwords/${encodeURIComponent(entryId)}`
|
|
46
|
+
: `/api/passwords/${encodeURIComponent(entryId)}`;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=secrets-config.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH agent protocol constants and framing helpers.
|
|
3
|
+
*
|
|
4
|
+
* Implements the subset of the SSH agent protocol needed for key listing
|
|
5
|
+
* and signing operations. Reference: draft-miller-ssh-agent.
|
|
6
|
+
*/
|
|
7
|
+
/** Client → Agent */
|
|
8
|
+
export declare const SSH2_AGENTC_REQUEST_IDENTITIES = 11;
|
|
9
|
+
export declare const SSH2_AGENTC_SIGN_REQUEST = 13;
|
|
10
|
+
/** Agent → Client */
|
|
11
|
+
export declare const SSH2_AGENT_FAILURE = 5;
|
|
12
|
+
export declare const SSH2_AGENT_IDENTITIES_ANSWER = 12;
|
|
13
|
+
export declare const SSH2_AGENT_SIGN_RESPONSE = 14;
|
|
14
|
+
export declare const SSH_AGENT_RSA_SHA2_256 = 2;
|
|
15
|
+
export declare const SSH_AGENT_RSA_SHA2_512 = 4;
|
|
16
|
+
/**
|
|
17
|
+
* Read a uint32-be from buffer at offset.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readUint32(buf: Buffer, offset: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Write a uint32-be into buffer at offset.
|
|
22
|
+
*/
|
|
23
|
+
export declare function writeUint32(buf: Buffer, offset: number, value: number): void;
|
|
24
|
+
/**
|
|
25
|
+
* Read an SSH "string" (uint32 length + data) from buffer at offset.
|
|
26
|
+
* Returns the data slice and the new offset after the string.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readString(buf: Buffer, offset: number): {
|
|
29
|
+
data: Buffer;
|
|
30
|
+
nextOffset: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Build an SSH "string" (uint32 length prefix + data).
|
|
34
|
+
*/
|
|
35
|
+
export declare function encodeString(data: Buffer | string): Buffer;
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a message body with the 4-byte length prefix.
|
|
38
|
+
*/
|
|
39
|
+
export declare function frameMessage(body: Buffer): Buffer;
|
|
40
|
+
/**
|
|
41
|
+
* Build an SSH_AGENT_FAILURE response.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildFailure(): Buffer;
|
|
44
|
+
/**
|
|
45
|
+
* Build an SSH2_AGENT_IDENTITIES_ANSWER response.
|
|
46
|
+
*
|
|
47
|
+
* @param keys Array of { publicKeyBlob, comment } pairs
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildIdentitiesAnswer(keys: {
|
|
50
|
+
publicKeyBlob: Buffer;
|
|
51
|
+
comment: string;
|
|
52
|
+
}[]): Buffer;
|
|
53
|
+
/**
|
|
54
|
+
* Build an SSH2_AGENT_SIGN_RESPONSE.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildSignResponse(signature: Buffer): Buffer;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH agent protocol constants and framing helpers.
|
|
3
|
+
*
|
|
4
|
+
* Implements the subset of the SSH agent protocol needed for key listing
|
|
5
|
+
* and signing operations. Reference: draft-miller-ssh-agent.
|
|
6
|
+
*/
|
|
7
|
+
// ─── Message types ────────────────────────────────────────────
|
|
8
|
+
/** Client → Agent */
|
|
9
|
+
export const SSH2_AGENTC_REQUEST_IDENTITIES = 11;
|
|
10
|
+
export const SSH2_AGENTC_SIGN_REQUEST = 13;
|
|
11
|
+
/** Agent → Client */
|
|
12
|
+
export const SSH2_AGENT_FAILURE = 5;
|
|
13
|
+
export const SSH2_AGENT_IDENTITIES_ANSWER = 12;
|
|
14
|
+
export const SSH2_AGENT_SIGN_RESPONSE = 14;
|
|
15
|
+
// ─── Signature algorithm flags (SSH2_AGENTC_SIGN_REQUEST flags field) ─
|
|
16
|
+
export const SSH_AGENT_RSA_SHA2_256 = 2;
|
|
17
|
+
export const SSH_AGENT_RSA_SHA2_512 = 4;
|
|
18
|
+
// ─── Framing helpers (pure functions) ─────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* Read a uint32-be from buffer at offset.
|
|
21
|
+
*/
|
|
22
|
+
export function readUint32(buf, offset) {
|
|
23
|
+
return buf.readUInt32BE(offset);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Write a uint32-be into buffer at offset.
|
|
27
|
+
*/
|
|
28
|
+
export function writeUint32(buf, offset, value) {
|
|
29
|
+
buf.writeUInt32BE(value, offset);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read an SSH "string" (uint32 length + data) from buffer at offset.
|
|
33
|
+
* Returns the data slice and the new offset after the string.
|
|
34
|
+
*/
|
|
35
|
+
export function readString(buf, offset) {
|
|
36
|
+
const len = readUint32(buf, offset);
|
|
37
|
+
const data = buf.subarray(offset + 4, offset + 4 + len);
|
|
38
|
+
return { data, nextOffset: offset + 4 + len };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build an SSH "string" (uint32 length prefix + data).
|
|
42
|
+
*/
|
|
43
|
+
export function encodeString(data) {
|
|
44
|
+
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
|
45
|
+
const result = Buffer.alloc(4 + buf.length);
|
|
46
|
+
result.writeUInt32BE(buf.length, 0);
|
|
47
|
+
buf.copy(result, 4);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wrap a message body with the 4-byte length prefix.
|
|
52
|
+
*/
|
|
53
|
+
export function frameMessage(body) {
|
|
54
|
+
const frame = Buffer.alloc(4 + body.length);
|
|
55
|
+
frame.writeUInt32BE(body.length, 0);
|
|
56
|
+
body.copy(frame, 4);
|
|
57
|
+
return frame;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build an SSH_AGENT_FAILURE response.
|
|
61
|
+
*/
|
|
62
|
+
export function buildFailure() {
|
|
63
|
+
const body = Buffer.alloc(1);
|
|
64
|
+
body[0] = SSH2_AGENT_FAILURE;
|
|
65
|
+
return frameMessage(body);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build an SSH2_AGENT_IDENTITIES_ANSWER response.
|
|
69
|
+
*
|
|
70
|
+
* @param keys Array of { publicKeyBlob, comment } pairs
|
|
71
|
+
*/
|
|
72
|
+
export function buildIdentitiesAnswer(keys) {
|
|
73
|
+
// Calculate total size: 1 (type) + 4 (nkeys) + per-key data
|
|
74
|
+
let size = 1 + 4;
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
size += 4 + key.publicKeyBlob.length; // string: public key blob
|
|
77
|
+
size += 4 + Buffer.byteLength(key.comment); // string: comment
|
|
78
|
+
}
|
|
79
|
+
const body = Buffer.alloc(size);
|
|
80
|
+
let offset = 0;
|
|
81
|
+
body[offset++] = SSH2_AGENT_IDENTITIES_ANSWER;
|
|
82
|
+
body.writeUInt32BE(keys.length, offset);
|
|
83
|
+
offset += 4;
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
body.writeUInt32BE(key.publicKeyBlob.length, offset);
|
|
86
|
+
offset += 4;
|
|
87
|
+
key.publicKeyBlob.copy(body, offset);
|
|
88
|
+
offset += key.publicKeyBlob.length;
|
|
89
|
+
const commentBuf = Buffer.from(key.comment);
|
|
90
|
+
body.writeUInt32BE(commentBuf.length, offset);
|
|
91
|
+
offset += 4;
|
|
92
|
+
commentBuf.copy(body, offset);
|
|
93
|
+
offset += commentBuf.length;
|
|
94
|
+
}
|
|
95
|
+
return frameMessage(body);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build an SSH2_AGENT_SIGN_RESPONSE.
|
|
99
|
+
*/
|
|
100
|
+
export function buildSignResponse(signature) {
|
|
101
|
+
// SSH2_AGENT_SIGN_RESPONSE: byte(14) + string(signature_blob)
|
|
102
|
+
const sigString = encodeString(signature);
|
|
103
|
+
const body = Buffer.alloc(1 + sigString.length);
|
|
104
|
+
body[0] = SSH2_AGENT_SIGN_RESPONSE;
|
|
105
|
+
sigString.copy(body, 1);
|
|
106
|
+
return frameMessage(body);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=ssh-agent-protocol.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH agent Unix domain socket server.
|
|
3
|
+
*
|
|
4
|
+
* Creates a Unix socket that implements the SSH agent protocol,
|
|
5
|
+
* serving keys from the passwd-sso vault.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Start the SSH agent socket server.
|
|
9
|
+
*
|
|
10
|
+
* @returns The socket path for SSH_AUTH_SOCK
|
|
11
|
+
*/
|
|
12
|
+
export declare function startAgent(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Stop the SSH agent socket server.
|
|
15
|
+
*/
|
|
16
|
+
export declare function stopAgent(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Get the current socket path, or null if not running.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getSocketPath(): string | null;
|